diff --git a/Human-Connection/.codecov.yml b/Human-Connection/.codecov.yml
new file mode 100644
index 000000000..2767ed675
--- /dev/null
+++ b/Human-Connection/.codecov.yml
@@ -0,0 +1,169 @@
+codecov:
+ #token: uuid # Your private repository token
+ #url: "http" # for Codecov Enterprise customers
+ #slug: "owner/repo" # for Codecov Enterprise customers
+ #branch: master # override the default branch
+ #bot: username # set user whom will be the consumer of oauth requests
+ #ci: # Custom CI domains if Codecov does not identify them automatically
+ # - ci.domain.com
+ # - !provider # ignore these providers when checking if CI passed
+ # # ex. You may test on Travis, Circle, and AppVeyor, but only need
+ # # to check if Travis passes. Therefore add: !circle and !appveyor
+ notify:
+ #after_n_builds: null # number of expected builds to recieve before sending notifications
+ # # after: check ci status unless disabled via require_ci_to_pass
+ require_ci_to_pass: yes # yes: will delay sending notifications until all ci is finished
+ # no: will send notifications without checking ci status and wait till "after_n_builds" are uploaded
+ #countdown: null # number of seconds to wait before first ci build check
+ #delay: null # number of seconds to wait between ci build checks
+
+coverage:
+ precision: 2 # 2 = xx.xx%, 0 = xx%
+ round: nearest # down|up|nearest - default down
+ # range: 50...60 # default 70...90. red...green
+
+ #notify:
+ # irc:
+ # default:
+ # server: "chat.freenode.net"|encrypted
+ # branches: null # all branches by default
+ # threshold: 1%
+ # message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
+ # flags: null
+ # paths: null
+ #
+ # slack:
+ # default:
+ # url: "http"|encrypted
+ # threshold: 1%
+ # branches: null # all branches by default
+ # message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
+ # attachments: "sunburst, diff"
+ # only_pulls: false
+ # flags: null
+ # paths: null
+ #
+ # email:
+ # default:
+ # to:
+ # - example@domain.com
+ # - &author
+ # threshold: 1%
+ # only_pulls: false
+ # layout: header, diff, trends
+ # flags: null
+ # paths: null
+ #
+ # hipchat:
+ # default:
+ # url: "http"|encrypted
+ # room: name|id
+ # threshold: 1%
+ # token: encrypted
+ # branches: null # all branches by default
+ # notify: false # if the hipchat message is silent or loud (default false)
+ # message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
+ # flags: null
+ # paths: null
+ #
+ # gitter:
+ # url: "http"|encrypted
+ # threshold: 1%
+ # branches: null # all branches by default
+ # message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
+ #
+ # webhooks:
+ # _name_:
+ # url: "http"|encrypted
+ # threshold: 1%
+ # branches: null # all branches by default
+
+ status:
+ project:
+ default: false # disable the default status that measures entire project
+ backend: # declare a new status context "backend"
+ against: parent
+ target: auto
+ threshold: null
+ #threshold: 1%
+ base: auto
+ if_no_uploads: error
+ if_not_found: success
+ if_ci_failed: error
+ only_pulls: false
+ #branches:
+ # - master
+ #flags:
+ # - integration
+ paths:
+ - backend/ # only include coverage in "backend/" folder
+ webapp: # declare a new status context "frontend"
+ against: parent
+ target: auto
+ threshold: null
+ #threshold: 1%
+ base: auto
+ if_no_uploads: error
+ if_not_found: success
+ if_ci_failed: error
+ only_pulls: false
+ #branches:
+ # - master
+ #flags:
+ # - integration
+ paths:
+ - webapp/ # only include coverage in "webapp/" folder
+
+ patch:
+ default: false
+ # against: parent
+ # target: 80%
+ # branches: null
+ # if_no_uploads: success
+ # if_not_found: success
+ # if_ci_failed: error
+ # only_pulls: false
+ # flags:
+ # - integration
+ # paths:
+ # - folder
+
+ #changes:
+ # default:
+ # against: parent
+ # branches: null
+ # if_no_uploads: error
+ # if_not_found: success
+ # if_ci_failed: error
+ # only_pulls: false
+ # flags:
+ # - integration
+ # paths:
+ # - folder
+
+ #flags:
+ # integration:
+ # branches:
+ # - master
+ # ignore:
+ # - app/ui
+
+ #ignore: # files and folders for processing
+ # - tests/*
+
+ #fixes:
+ # - "old_path::new_path"
+
+comment:
+ # layout options are quite limited in v4.x - there have been way more options in v1.0
+ layout: reach, diff, flags, files # mostly old options: header, diff, uncovered, reach, files, tree, changes, sunburst, flags
+ behavior: new # default = posts once then update, posts new if delete
+ # once = post once then updates
+ # new = delete old, post new
+ # spammy = post new
+ require_changes: false # if true: only post the comment if coverage changes
+ require_base: no # [yes :: must have a base report to post]
+ require_head: no # [yes :: must have a head report to post]
+ branches: null # branch names that can post comment
+ flags: null
+ paths: null
\ No newline at end of file
diff --git a/Human-Connection/.gitbook.yaml b/Human-Connection/.gitbook.yaml
new file mode 100644
index 000000000..16680a37a
--- /dev/null
+++ b/Human-Connection/.gitbook.yaml
@@ -0,0 +1,3 @@
+structure:
+ readme: README.md
+ summary: SUMMARY.md
diff --git a/Human-Connection/.gitbook/assets/grafik (1).png b/Human-Connection/.gitbook/assets/grafik (1).png
new file mode 100644
index 000000000..8fabb8555
Binary files /dev/null and b/Human-Connection/.gitbook/assets/grafik (1).png differ
diff --git a/Human-Connection/.gitbook/assets/grafik-1 (1).png b/Human-Connection/.gitbook/assets/grafik-1 (1).png
new file mode 100644
index 000000000..cc5dade55
Binary files /dev/null and b/Human-Connection/.gitbook/assets/grafik-1 (1).png differ
diff --git a/Human-Connection/.gitbook/assets/grafik-1.png b/Human-Connection/.gitbook/assets/grafik-1.png
new file mode 100644
index 000000000..cc5dade55
Binary files /dev/null and b/Human-Connection/.gitbook/assets/grafik-1.png differ
diff --git a/Human-Connection/.gitbook/assets/grafik-4.png b/Human-Connection/.gitbook/assets/grafik-4.png
new file mode 100644
index 000000000..dab3eef27
Binary files /dev/null and b/Human-Connection/.gitbook/assets/grafik-4.png differ
diff --git a/Human-Connection/.gitbook/assets/grafik.png b/Human-Connection/.gitbook/assets/grafik.png
new file mode 100644
index 000000000..8fabb8555
Binary files /dev/null and b/Human-Connection/.gitbook/assets/grafik.png differ
diff --git a/Human-Connection/.gitbook/assets/graphql-playground (1).png b/Human-Connection/.gitbook/assets/graphql-playground (1).png
new file mode 100644
index 000000000..32396a577
Binary files /dev/null and b/Human-Connection/.gitbook/assets/graphql-playground (1).png differ
diff --git a/Human-Connection/.gitbook/assets/graphql-playground.png b/Human-Connection/.gitbook/assets/graphql-playground.png
new file mode 100644
index 000000000..32396a577
Binary files /dev/null and b/Human-Connection/.gitbook/assets/graphql-playground.png differ
diff --git a/Human-Connection/.gitbook/assets/humanconnection (1).png b/Human-Connection/.gitbook/assets/humanconnection (1).png
new file mode 100644
index 000000000..f0576413f
Binary files /dev/null and b/Human-Connection/.gitbook/assets/humanconnection (1).png differ
diff --git a/Human-Connection/.gitbook/assets/humanconnection.png b/Human-Connection/.gitbook/assets/humanconnection.png
new file mode 100644
index 000000000..f0576413f
Binary files /dev/null and b/Human-Connection/.gitbook/assets/humanconnection.png differ
diff --git a/Human-Connection/.gitbook/assets/lets_get_together.png b/Human-Connection/.gitbook/assets/lets_get_together.png
new file mode 100644
index 000000000..07017e489
Binary files /dev/null and b/Human-Connection/.gitbook/assets/lets_get_together.png differ
diff --git a/Human-Connection/.gitbook/assets/screenshot (1).png b/Human-Connection/.gitbook/assets/screenshot (1).png
new file mode 100644
index 000000000..b4ff4b2f9
Binary files /dev/null and b/Human-Connection/.gitbook/assets/screenshot (1).png differ
diff --git a/Human-Connection/.gitbook/assets/screenshot-neo4j-download-center-current-releases.png b/Human-Connection/.gitbook/assets/screenshot-neo4j-download-center-current-releases.png
new file mode 100644
index 000000000..8d9033864
Binary files /dev/null and b/Human-Connection/.gitbook/assets/screenshot-neo4j-download-center-current-releases.png differ
diff --git a/Human-Connection/.gitbook/assets/screenshot-styleguide (1).png b/Human-Connection/.gitbook/assets/screenshot-styleguide (1).png
new file mode 100644
index 000000000..d8e009394
Binary files /dev/null and b/Human-Connection/.gitbook/assets/screenshot-styleguide (1).png differ
diff --git a/Human-Connection/.gitbook/assets/screenshot-styleguide (2).png b/Human-Connection/.gitbook/assets/screenshot-styleguide (2).png
new file mode 100644
index 000000000..d8e009394
Binary files /dev/null and b/Human-Connection/.gitbook/assets/screenshot-styleguide (2).png differ
diff --git a/Human-Connection/.gitbook/assets/screenshot-styleguide.png b/Human-Connection/.gitbook/assets/screenshot-styleguide.png
new file mode 100644
index 000000000..d8e009394
Binary files /dev/null and b/Human-Connection/.gitbook/assets/screenshot-styleguide.png differ
diff --git a/Human-Connection/.gitbook/assets/screenshot.png b/Human-Connection/.gitbook/assets/screenshot.png
new file mode 100644
index 000000000..b4ff4b2f9
Binary files /dev/null and b/Human-Connection/.gitbook/assets/screenshot.png differ
diff --git a/Human-Connection/.github/ISSUE_TEMPLATE.md b/Human-Connection/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..9bbd6de90
--- /dev/null
+++ b/Human-Connection/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,11 @@
+
+
+## 💬 Issue
+
+
+
diff --git a/Human-Connection/.github/ISSUE_TEMPLATE/bug_report.md b/Human-Connection/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..fbf7173fc
--- /dev/null
+++ b/Human-Connection/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,32 @@
+---
+name: 🐛 Bug report
+about: Create a report to help us improve
+labels: bug
+title: 🐛 [Bug]
+---
+
+## :bug: Bugreport
+
+
+
+### Steps to reproduce the behavior
+1.
+2.
+3.
+4. ...
+5. Profit
+
+
+### Expected behavior
+
+
+
+### Version & Environment
+ Type: []
+ - OS: []
+ - Browser: []
+ - Version []
+ - Device: []
+
+### Additional context
+
diff --git a/Human-Connection/.github/ISSUE_TEMPLATE/feature_request.md b/Human-Connection/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..1fba3fa58
--- /dev/null
+++ b/Human-Connection/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,16 @@
+---
+name: 🚀 Feature request
+about: Suggest an idea for this project
+labels: feature
+title: 🚀 [Feature]
+---
+
+## :rocket: Feature
+
+
+### Design & Layout
+
+
+
+### Additional context
+
diff --git a/Human-Connection/.github/ISSUE_TEMPLATE/question.md b/Human-Connection/.github/ISSUE_TEMPLATE/question.md
new file mode 100644
index 000000000..aabbc0f0a
--- /dev/null
+++ b/Human-Connection/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,12 @@
+---
+name: 💬 Question
+about: If you need help understanding HumanConnection.
+labels: question
+title: 💬 [Question]
+---
+
+
+
+## :speech_balloon: Question
+
diff --git a/Human-Connection/.github/PULL_REQUEST_TEMPLATE.md b/Human-Connection/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..6cfe4c30c
--- /dev/null
+++ b/Human-Connection/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,13 @@
+## 🍰 Pullrequest
+
+
+### Issues
+
+- None
+
+### Todo
+
+- [X] None
diff --git a/Human-Connection/.gitignore b/Human-Connection/.gitignore
new file mode 100644
index 000000000..884ce422e
--- /dev/null
+++ b/Human-Connection/.gitignore
@@ -0,0 +1,19 @@
+.env
+.idea
+*.iml
+.DS_Store
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.yarn-integrity
+.eslintcache
+kubeconfig.yaml
+
+node_modules/
+cypress/videos
+cypress/screenshots/
+cypress.env.json
+
+!.gitkeep
+**/coverage
+
diff --git a/Human-Connection/.travis.yml b/Human-Connection/.travis.yml
new file mode 100644
index 000000000..f48b0bb36
--- /dev/null
+++ b/Human-Connection/.travis.yml
@@ -0,0 +1,70 @@
+dist: xenial
+language: generic
+addons:
+ apt:
+ packages:
+ - libgconf-2-4
+ snaps:
+ - docker
+ - chromium
+
+before_install:
+ - yarn global add wait-on
+ # Install Codecov
+ - yarn global add codecov
+ - yarn install
+ - cp cypress.env.template.json cypress.env.json
+
+install:
+ - docker-compose -f docker-compose.yml -f docker-compose.travis.yml up --build -d
+ # avoid "Database constraints have changed after this transaction started"
+ - wait-on http://localhost:7474
+
+script:
+ - export CYPRESS_RETRIES=1
+ - export BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then echo $TRAVIS_BRANCH; else echo $TRAVIS_PULL_REQUEST_BRANCH; fi)
+ - echo "TRAVIS_BRANCH=$TRAVIS_BRANCH, PR=$PR, BRANCH=$BRANCH"
+ # Backend
+ - docker-compose exec backend yarn run lint
+ - docker-compose exec backend yarn run test:jest --ci --verbose=false --coverage
+ - docker-compose exec backend yarn run db:reset
+ - docker-compose exec backend yarn run db:seed
+ - docker-compose exec backend yarn run test:cucumber --tags "not @wip"
+ - docker-compose exec backend yarn run db:reset
+ - docker-compose exec backend yarn run db:seed
+ # Frontend
+ - docker-compose exec webapp yarn run lint
+ - docker-compose exec webapp yarn run test --ci --verbose=false --coverage
+ - docker-compose exec -d backend yarn run test:before:seeder
+ # Fullstack
+ - yarn run cypress:run
+ # Coverage
+ - codecov
+
+after_success:
+ - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
+ - chmod +x send.sh
+ - ./send.sh success $WEBHOOK_URL
+ - if [ $TRAVIS_BRANCH == "master" ] && [ $TRAVIS_EVENT_TYPE == "push" ]; then
+ wget https://raw.githubusercontent.com/Human-Connection/Discord-Bot/develop/tester.sh &&
+ chmod +x tester.sh &&
+ ./tester.sh staging $WEBHOOK_URL;
+ fi
+
+after_failure:
+ - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
+ - chmod +x send.sh
+ - ./send.sh failure $WEBHOOK_URL
+
+before_deploy:
+ - ./scripts/setup_kubernetes.sh
+
+deploy:
+ - provider: script
+ script: scripts/docker_push.sh
+ on:
+ branch: master
+ - provider: script
+ script: scripts/deploy.sh
+ on:
+ branch: master
diff --git a/Human-Connection/.vscode/extensions.json b/Human-Connection/.vscode/extensions.json
new file mode 100644
index 000000000..e2d92ff83
--- /dev/null
+++ b/Human-Connection/.vscode/extensions.json
@@ -0,0 +1,7 @@
+{
+ "recommendations": [
+ "dbaeumer.vscode-eslint",
+ "octref.vetur",
+ "gruntfuggly.todo-tree",
+ ]
+}
\ No newline at end of file
diff --git a/Human-Connection/.vscode/settings.json b/Human-Connection/.vscode/settings.json
new file mode 100644
index 000000000..e2a727871
--- /dev/null
+++ b/Human-Connection/.vscode/settings.json
@@ -0,0 +1,12 @@
+{
+ "eslint.validate": [
+ "javascript",
+ "javascriptreact",
+ {
+ "language": "vue",
+ "autoFix": true
+ }
+ ],
+ "editor.formatOnSave": true,
+ "eslint.autoFixOnSave": true
+}
diff --git a/Human-Connection/CODE_OF_CONDUCT.md b/Human-Connection/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..b331a1736
--- /dev/null
+++ b/Human-Connection/CODE_OF_CONDUCT.md
@@ -0,0 +1,44 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at developer@human-connection.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.4, available at [http://contributor-covenant.org/version/1/4](http://contributor-covenant.org/version/1/4/)
+
diff --git a/Human-Connection/CONTRIBUTING.md b/Human-Connection/CONTRIBUTING.md
new file mode 100644
index 000000000..19aaf3301
--- /dev/null
+++ b/Human-Connection/CONTRIBUTING.md
@@ -0,0 +1,80 @@
+# CONTRIBUTING
+
+Thanks so much for thinking of contributing to the Human Connection project, we really appreciate it! :-\)
+
+## Getting Set Up
+
+Instructions for how to install all the necessary software can be found in our [documentation](https://docs.human-connection.org/human-connection/)
+
+We recommend that new folks should ideally work together with an existing developer. Please join our discord instance to chat with developers or just ask them in tickets in [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f/boards?repos=152252353):
+
+
+
+Here are some general notes on our development flow:
+
+## Development
+
+* Currently operating in two week sprints
+* We are using ZenHub to coordinate
+ * estimating time per issue is the crucial feature of [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f) that Github does not have
+ * "up-for-grabs" links to [Github project](https://github.com/orgs/Human-Connection/projects/10?card_filter_query=label%3A"good+first+issue)
+ * ordering on ZenHub not necessarily reflected on github projects
+* AgileVentures run open pairing sessions at 10:30am UTC each week on Tuesdays and Thursdays
+* Core team
+ * all the people who are hired by HC non-profit corporation
+ * you can Meet-the-team [every two weeks in German](https://human-connection.org/veranstaltungen/) and [every month in English](https://human-connection.org/en/events/).
+ * 9 people
+ * 2 core developers \(Robert [@roschaefer](https://github.com/roschaefer) and Greg [@appinteractive](https://github.com/appinteractive)\)
+ * 3 marketeers Jasi, Dennis and Sensi
+ * Hardy doing business development
+ * Martin head of IT and previously data protection officer
+ * Victor doing accounting and controlling
+ * Nicolas is the community manager \(reviews content in the network\) reflects community opinion back to the core team
+* when can folks pair with Robert
+ * 10am UTC until 5pm UTC every working day
+
+## Philosophy
+
+We practise [collective code ownership](http://www.extremeprogramming.org/rules/collective.html) rather than strong code ownership, which means that:
+
+* anyone can start working on anyone elses code
+* we avoid blocking because someone else isn't working on something
+* however it's sometimes good to leave something in order to create successful education experience
+* everyone should always push their code to branches so others can see it
+
+Everyone feel free to request merges or answers to issues from the project managers
+
+But what do we do when waiting for merge into master \(wanting to keep PRs small\) --> Robert recommends creating a pull request for each step
+
+* programming is also about thinking about other people - empathy for your co-workers
+ * but what about when you are waiting for merge?
+ * solutions
+ * 1\) put 2nd PR into branch that the first PR is hitting - but requires update after merging
+ * 2\) prefer to leave exiting PR until it can be reviewed, and instead go and work on some other part of the codebase that is not impacted by the first PR
+
+### Code Review
+* Github setting in place - at least one review is required to merge
+ - in principle anyone (who is not the PR owner) can review
+ - but often it will be the core developers (Robert, Ulf, Greg, Wolfgang?)
+ - once there is a review, and presuming no requested changes, PR opener can merge
+
+* CI/tests
+ - the CI needs to pass
+ - linting <-- autofix?
+ - tests (unit, feature) (backend, frontend)
+ - codecoverage
+
+## Notes
+
+question: when you want to pick a task - \(find out priority\) - is it in discord? is it in AV slack? --> Robert says you can always ask in discord - group channels are the best
+
+Robert shares: [Zenhub board](https://app.zenhub.com/workspaces/nitro-embed-5c0154ecc699f60fc92cf11f/boards?repos=112590397,152252353,152252578,157710732,163305928) Robert says the order of tickets are preserved in ZenHub and reflect their priority \(most important at the top\) and so check out the current milestones
+
+Matt - question about who can work on [ticket 100](https://app.zenhub.com/workspaces/nitro-embed-5c0154ecc699f60fc92cf11f/issues/human-connection/human-connection/100) --> Robert - in rare occasions it might be exclusive to someone with admin permissions Robert: notes greg just pushed this today: [https://github.com/Human-Connection/Nitro-Deployment](https://github.com/Human-Connection/Nitro-Deployment)
+
+Matt makes point that new stories will have to be taken off the "New Issues" and Robert says that's fine, if you don't like the first one, then you can take the next one. Volunteeers have no commitment except their own self development and their awesomeness by contributing to free and open-source software projects.
+
+Robert notes that everyone is invited to join the kickoff meetings
+
+Robert - difference between "important" \(creates a lot of value\) and "beginner friendly" \(easy to implement\)
+
diff --git a/Human-Connection/LICENSE.md b/Human-Connection/LICENSE.md
new file mode 100644
index 000000000..646ae3a6d
--- /dev/null
+++ b/Human-Connection/LICENSE.md
@@ -0,0 +1,12 @@
+# LICENSE
+
+MIT License
+
+Copyright \(c\) 2018 Human-Connection gGmbH
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files \(the "Software"\), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/Human-Connection/README.md b/Human-Connection/README.md
new file mode 100644
index 000000000..ac7d2a024
--- /dev/null
+++ b/Human-Connection/README.md
@@ -0,0 +1,58 @@
+# Human-Connection
+
+[](https://travis-ci.com/Human-Connection/Human-Connection)
+[](https://codecov.io/gh/Human-Connection/Human-Connection/)
+[](https://github.com/Human-Connection/Nitro-Backend/blob/backend/LICENSE.md)
+[](https://discord.gg/6ub73U3)
+
+Human Connection is a nonprofit social, action and knowledge network that connects information to action and promotes positive local and global change in all areas of life.
+
+* **Social**: Interact with other people not just by commenting their posts, but by providing **Pro & Contra** arguments, give a **Versus** or ask them by integrated **Chat** or **Let's Talk**
+* **Knowledge**: Read articles about interesting topics and find related posts in the **More Info** tab or by **Filtering** based on **Categories** and **Tagging** or by using the **Fulltext Search**.
+* **Action**: Don't just read about how to make the world a better place, but come into **Action** by following provided suggestions on the **Action** tab provided by other people or **Organisations**.
+
+ [](https://human-connection.org)
+
+**Technology Stack**
+
+* [VueJS](https://vuejs.org/)
+* [NuxtJS](https://nuxtjs.org/)
+* [GraphQL](https://graphql.org/)
+* [NodeJS](https://nodejs.org/en/)
+* [Neo4J](https://neo4j.com/)
+
+
+## Live demo
+
+Try out our deployed [staging environment](https://nitro-staging.human-connection.org/).
+
+Logins:
+
+| email | password | role |
+| :--- | :--- | :--- |
+| `user@example.org` | 1234 | user |
+| `moderator@example.org` | 1234 | moderator |
+| `admin@example.org` | 1234 | admin |
+
+## Documentation
+
+Learn how to set up a local development environment in our [Docs](https://docs.human-connection.org/human-connection/) :mag_right:
+
+## Translations
+
+You can help translating the interface by joining us on [lokalise.co](https://lokalise.co/public/556252725c18dd752dd546.13222042/).
+Thank you lokalise for providing us with a premium account :raised_hands:.
+
+## Developer Chat
+
+Join our friendly open-source community on [Discord](https://discord.gg/6ub73U3) :heart_eyes_cat:
+Just introduce yourself at `#user-presentation` and mention `@@Mentor` to get you onboard :neckbeard:
+Check out the [contribution guideline](./CONTRIBUTING.md), too!
+
+
+## Attributions
+
+Locale Icons made by [Freepik](http://www.freepik.com/) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/)
+
+## License
+See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).
diff --git a/Human-Connection/SUMMARY.md b/Human-Connection/SUMMARY.md
new file mode 100644
index 000000000..c281e2fae
--- /dev/null
+++ b/Human-Connection/SUMMARY.md
@@ -0,0 +1,40 @@
+# Table of contents
+
+* [Introduction](README.md)
+* [Edit this Documentation](edit-this-documentation.md)
+* [Installation](installation.md)
+* [Neo4J](neo4j/README.md)
+* [Backend](backend/README.md)
+ * [GraphQL](backend/graphql.md)
+* [Webapp](webapp/README.md)
+ * [COMPONENTS](webapp/components.md)
+ * [PLUGINS](webapp/plugins.md)
+ * [STORE](webapp/store.md)
+ * [PAGES](webapp/pages.md)
+ * [ASSETS](webapp/assets.md)
+ * [LAYOUTS](webapp/layouts.md)
+ * [Styleguide](webapp/styleguide.md)
+ * [STATIC](webapp/static.md)
+ * [MIDDLEWARE](webapp/middleware.md)
+* [Testing Guide](testing.md)
+ * [End-to-end tests](cypress/README.md)
+ * [Frontend tests](webapp/testing.md)
+ * [Backend tests](backend/testing.md)
+* [Contributing](CONTRIBUTING.md)
+* [Kubernetes Deployment](deployment/README.md)
+ * [Minikube](deployment/minikube/README.md)
+ * [Digital Ocean](deployment/digital-ocean/README.md)
+ * [Kubernetes Dashboard](deployment/digital-ocean/dashboard/README.md)
+ * [HTTPS](deployment/digital-ocean/https/README.md)
+ * [Human Connection](deployment/human-connection/README.md)
+ * [Mailserver](deployment/human-connection/mailserver/README.md)
+ * [Volumes](deployment/volumes/README.md)
+ * [Neo4J Offline-Backups](deployment/volumes/neo4j-offline-backup/README.md)
+ * [Volume Snapshots](deployment/volumes/volume-snapshots/README.md)
+ * [Reclaim Policy](deployment/volumes/reclaim-policy/README.md)
+ * [Velero](deployment/volumes/velero/README.md)
+ * [Legacy Migration](deployment/legacy-migration/README.md)
+* [Feature Specification](cypress/features.md)
+* [Code of conduct](CODE_OF_CONDUCT.md)
+* [License](LICENSE.md)
+
diff --git a/Human-Connection/backend/.babelrc b/Human-Connection/backend/.babelrc
new file mode 100644
index 000000000..f36dbeadb
--- /dev/null
+++ b/Human-Connection/backend/.babelrc
@@ -0,0 +1,15 @@
+{
+ "presets": [
+ [
+ "@babel/preset-env",
+ {
+ "targets": {
+ "node": "10"
+ }
+ }
+ ]
+ ],
+ "plugins": [
+ "@babel/plugin-proposal-throw-expressions"
+ ]
+}
diff --git a/Human-Connection/backend/.codecov.yml b/Human-Connection/backend/.codecov.yml
new file mode 100644
index 000000000..97bec0084
--- /dev/null
+++ b/Human-Connection/backend/.codecov.yml
@@ -0,0 +1,2 @@
+coverage:
+ range: "60...100"
diff --git a/Human-Connection/backend/.dockerignore b/Human-Connection/backend/.dockerignore
new file mode 100644
index 000000000..25a941824
--- /dev/null
+++ b/Human-Connection/backend/.dockerignore
@@ -0,0 +1,22 @@
+.vscode/
+.nyc_output/
+.github/
+.travis.yml
+.graphqlconfig
+.env
+
+Dockerfile
+docker-compose*.yml
+
+./*.png
+./*.log
+
+node_modules/
+scripts/
+dist/
+
+maintenance-worker/
+neo4j/
+
+public/uploads/*
+!.gitkeep
diff --git a/Human-Connection/backend/.env.template b/Human-Connection/backend/.env.template
new file mode 100644
index 000000000..0c80529a1
--- /dev/null
+++ b/Human-Connection/backend/.env.template
@@ -0,0 +1,17 @@
+NEO4J_URI=bolt://localhost:7687
+NEO4J_USER=neo4j
+NEO4J_PASSWORD=letmein
+GRAPHQL_PORT=4000
+GRAPHQL_URI=http://localhost:4000
+CLIENT_URI=http://localhost:3000
+MOCKS=false
+SMTP_HOST=
+SMTP_PORT=
+SMTP_IGNORE_TLS=true
+SMTP_USERNAME=
+SMTP_PASSWORD=
+
+JWT_SECRET="b/&&7b78BF&fv/Vd"
+MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"
+
+PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"
diff --git a/Human-Connection/backend/.eslintrc.js b/Human-Connection/backend/.eslintrc.js
new file mode 100644
index 000000000..0000bb066
--- /dev/null
+++ b/Human-Connection/backend/.eslintrc.js
@@ -0,0 +1,25 @@
+module.exports = {
+ env: {
+ es6: true,
+ node: true,
+ jest: true
+ },
+ parserOptions: {
+ parser: 'babel-eslint'
+ },
+ extends: [
+ 'standard',
+ 'plugin:prettier/recommended'
+ ],
+ plugins: [
+ 'jest'
+ ],
+ rules: {
+ //'indent': [ 'error', 2 ],
+ //'quotes': [ "error", "single"],
+ // 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+ 'no-console': ['error'],
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+ 'prettier/prettier': ['error'],
+ },
+};
diff --git a/Human-Connection/backend/.gitignore b/Human-Connection/backend/.gitignore
new file mode 100644
index 000000000..81a29c8e6
--- /dev/null
+++ b/Human-Connection/backend/.gitignore
@@ -0,0 +1,13 @@
+node_modules/
+.env
+.vscode
+.idea
+yarn-error.log
+dist/*
+coverage.lcov
+.nyc_output/
+public/uploads/*
+!.gitkeep
+
+# Apple macOS folder attribute file
+.DS_Store
\ No newline at end of file
diff --git a/Human-Connection/backend/.graphqlconfig b/Human-Connection/backend/.graphqlconfig
new file mode 100644
index 000000000..ca328bc83
--- /dev/null
+++ b/Human-Connection/backend/.graphqlconfig
@@ -0,0 +1,3 @@
+{
+ "schemaPath": "./src/schema.graphql"
+}
diff --git a/Human-Connection/backend/.prettierrc.js b/Human-Connection/backend/.prettierrc.js
new file mode 100644
index 000000000..e2cf91e91
--- /dev/null
+++ b/Human-Connection/backend/.prettierrc.js
@@ -0,0 +1,9 @@
+
+module.exports = {
+ semi: false,
+ printWidth: 100,
+ singleQuote: true,
+ trailingComma: "all",
+ tabWidth: 2,
+ bracketSpacing: true
+};
diff --git a/Human-Connection/backend/Dockerfile b/Human-Connection/backend/Dockerfile
new file mode 100644
index 000000000..2e8667461
--- /dev/null
+++ b/Human-Connection/backend/Dockerfile
@@ -0,0 +1,28 @@
+FROM node:12.5-alpine as base
+LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
+
+EXPOSE 4000
+ARG BUILD_COMMIT
+ENV BUILD_COMMIT=$BUILD_COMMIT
+ARG WORKDIR=/nitro-backend
+RUN mkdir -p $WORKDIR
+WORKDIR $WORKDIR
+
+RUN apk --no-cache add git
+
+COPY package.json yarn.lock ./
+COPY .env.template .env
+CMD ["yarn", "run", "start"]
+
+FROM base as builder
+RUN yarn install --frozen-lockfile --non-interactive
+COPY . .
+RUN cp .env.template .env
+RUN yarn run build
+
+# reduce image size with a multistage build
+FROM base as production
+ENV NODE_ENV=production
+COPY --from=builder /nitro-backend/dist ./dist
+COPY ./public/img/ ./public/img/
+RUN yarn install --frozen-lockfile --non-interactive
diff --git a/Human-Connection/backend/README.md b/Human-Connection/backend/README.md
new file mode 100644
index 000000000..cd56e231f
--- /dev/null
+++ b/Human-Connection/backend/README.md
@@ -0,0 +1,130 @@
+# Backend
+
+## Installation with Docker
+
+Run the following command to install everything through docker.
+
+The installation takes a bit longer on the first pass or on rebuild ...
+
+```bash
+$ docker-compose up
+
+# rebuild the containers for a cleanup
+$ docker-compose up --build
+```
+
+Wait a little until your backend is up and running at [http://localhost:4000/](http://localhost:4000/).
+
+## Installation without Docker
+
+For the local installation you need a recent version of [node](https://nodejs.org/en/)
+(>= `v10.12.0`).
+
+Install node dependencies with [yarn](https://yarnpkg.com/en/):
+```bash
+$ cd backend
+$ yarn install
+```
+
+Copy Environment Variables:
+```bash
+# in backend/
+$ cp .env.template .env
+```
+Configure the new file according to your needs and your local setup. Make sure
+a [local Neo4J](http://localhost:7474) instance is up and running.
+
+Start the backend for development with:
+```bash
+$ yarn run dev
+```
+
+or start the backend in production environment with:
+```bash
+yarn run start
+```
+
+For e-mail delivery, please configure at least `SMTP_HOST` and `SMTP_PORT` in
+your `.env` configuration file.
+
+Your backend is up and running at [http://localhost:4000/](http://localhost:4000/)
+This will start the GraphQL service \(by default on localhost:4000\) where you
+can issue GraphQL requests or access GraphQL Playground in the browser.
+
+
+
+
+#### Seed Database
+
+If you want your backend to return anything else than an empty response, you
+need to seed your database:
+
+{% tabs %}
+{% tab title="Docker" %}
+
+In another terminal run:
+```bash
+$ docker-compose exec backend yarn run db:seed
+```
+
+To reset the database run:
+```bash
+$ docker-compose exec backend yarn run db:reset
+# you could also wipe out your neo4j database and delete all volumes with:
+$ docker-compose down -v
+```
+{% endtab %}
+
+{% tab title="Without Docker" %}
+Run:
+```bash
+$ yarn run db:seed
+```
+
+To reset the database run:
+```bash
+$ yarn run db:reset
+```
+{% endtab %}
+{% endtabs %}
+
+
+# Testing
+
+**Beware**: We have no multiple database setup at the moment. We clean the
+database after each test, running the tests will wipe out all your data!
+
+
+{% tabs %}
+{% tab title="Docker" %}
+
+Run the _**jest**_ tests:
+
+```bash
+$ docker-compose exec backend yarn run test:jest
+```
+
+Run the _**cucumber**_ features:
+
+```bash
+$ docker-compose exec backend yarn run test:cucumber
+```
+
+{% endtab %}
+
+{% tab title="Without Docker" %}
+
+Run the _**jest**_ tests:
+
+```bash
+$ yarn run test:jest
+```
+
+Run the _**cucumber**_ features:
+
+```bash
+$ yarn run test:cucumber
+```
+
+{% endtab %}
+{% endtabs %}
diff --git a/Human-Connection/backend/graphql-playground.png b/Human-Connection/backend/graphql-playground.png
new file mode 100644
index 000000000..32396a577
Binary files /dev/null and b/Human-Connection/backend/graphql-playground.png differ
diff --git a/Human-Connection/backend/graphql.md b/Human-Connection/backend/graphql.md
new file mode 100644
index 000000000..12cc59e57
--- /dev/null
+++ b/Human-Connection/backend/graphql.md
@@ -0,0 +1,18 @@
+# GraphQL with Apollo
+
+GraphQL is a data query language which provides an alternative to REST and ad-hoc web service architectures. It allows clients to define the structure of the data required, and exactly the same structure of the data is returned from the server.
+
+
+
+## Middleware keeps resolvers clean
+
+
+
+
+A well-organized codebase is key for the ability to maintain and easily introduce changes into an app. Figuring out the right structure for your code remains a continuous challenge - especially as an application grows and more developers are joining a project.
+
+A common problem in GraphQL servers is that resolvers often get cluttered with business logic, making the entire resolver system harder to understand and maintain.
+
+GraphQL Middleware uses the [_middleware pattern_](https://dzone.com/articles/understanding-middleware-pattern-in-expressjs) \(well-known from Express.js\) to pull out repetitive code from resolvers and execute it before or after one of your resolvers is invoked. This improves code modularity and keeps your resolvers clean and simple.
+
+
diff --git a/Human-Connection/backend/humanconnection.png b/Human-Connection/backend/humanconnection.png
new file mode 100644
index 000000000..f0576413f
Binary files /dev/null and b/Human-Connection/backend/humanconnection.png differ
diff --git a/Human-Connection/backend/package.json b/Human-Connection/backend/package.json
new file mode 100644
index 000000000..599e8eac6
--- /dev/null
+++ b/Human-Connection/backend/package.json
@@ -0,0 +1,112 @@
+{
+ "name": "human-connection-backend",
+ "version": "0.0.1",
+ "description": "GraphQL Backend for Human Connection",
+ "main": "src/index.js",
+ "scripts": {
+ "build": "babel src/ -d dist/ --copy-files",
+ "start": "node dist/",
+ "dev": "nodemon --exec babel-node src/ -e js,gql",
+ "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,gql",
+ "lint": "eslint src --config .eslintrc.js",
+ "test": "run-s test:jest test:cucumber",
+ "test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 yarn run dev 2> /dev/null",
+ "test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev 2> /dev/null",
+ "test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand",
+ "test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
+ "test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand",
+ "test:jest": "run-p --race test:before:* \"test:jest:cmd {@}\" --",
+ "test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --",
+ "test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --",
+ "db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js",
+ "db:reset": "cross-env babel-node src/seed/reset-db.js",
+ "db:seed": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions run-p --race dev db:script:seed"
+ },
+ "author": "Human Connection gGmbH",
+ "license": "MIT",
+ "jest": {
+ "verbose": true,
+ "collectCoverageFrom": [
+ "**/*.js",
+ "!**/node_modules/**",
+ "!**/test/**",
+ "!**/dist/**",
+ "!**/src/**/?(*.)+(spec|test).js?(x)"
+ ],
+ "coverageReporters": [
+ "text",
+ "lcov"
+ ],
+ "testMatch": [
+ "**/src/**/?(*.)+(spec|test).js?(x)"
+ ]
+ },
+ "dependencies": {
+ "activitystrea.ms": "~2.1.3",
+ "apollo-cache-inmemory": "~1.6.2",
+ "apollo-client": "~2.6.3",
+ "apollo-link-context": "~1.0.18",
+ "apollo-link-http": "~1.5.15",
+ "apollo-server": "~2.6.7",
+ "bcryptjs": "~2.4.3",
+ "cheerio": "~1.0.0-rc.3",
+ "cors": "~2.8.5",
+ "cross-env": "~5.2.0",
+ "date-fns": "2.0.0-beta.2",
+ "debug": "~4.1.1",
+ "dotenv": "~8.0.0",
+ "express": "~4.17.1",
+ "faker": "Marak/faker.js#master",
+ "graphql": "~14.4.0",
+ "graphql-custom-directives": "~0.2.14",
+ "graphql-iso-date": "~3.6.1",
+ "graphql-middleware": "~3.0.2",
+ "graphql-shield": "~6.0.2",
+ "graphql-tag": "~2.10.1",
+ "graphql-yoga": "~1.18.0",
+ "helmet": "~3.18.0",
+ "jsonwebtoken": "~8.5.1",
+ "linkifyjs": "~2.1.8",
+ "lodash": "~4.17.11",
+ "merge-graphql-schemas": "^1.5.8",
+ "neo4j-driver": "~1.7.4",
+ "neo4j-graphql-js": "^2.6.3",
+ "node-fetch": "~2.6.0",
+ "nodemailer": "^6.2.1",
+ "npm-run-all": "~4.1.5",
+ "request": "~2.88.0",
+ "sanitize-html": "~1.20.1",
+ "slug": "~1.1.0",
+ "trunc-html": "~1.1.2",
+ "uuid": "~3.3.2",
+ "wait-on": "~3.2.0"
+ },
+ "devDependencies": {
+ "@babel/cli": "~7.4.4",
+ "@babel/core": "~7.4.5",
+ "@babel/node": "~7.4.5",
+ "@babel/plugin-proposal-throw-expressions": "^7.2.0",
+ "@babel/preset-env": "~7.4.5",
+ "@babel/register": "~7.4.4",
+ "apollo-server-testing": "~2.6.7",
+ "babel-core": "~7.0.0-0",
+ "babel-eslint": "~10.0.2",
+ "babel-jest": "~24.8.0",
+ "chai": "~4.2.0",
+ "cucumber": "~5.1.0",
+ "eslint": "~6.0.1",
+ "eslint-config-prettier": "~6.0.0",
+ "eslint-config-standard": "~12.0.0",
+ "eslint-plugin-import": "~2.18.0",
+ "eslint-plugin-jest": "~22.7.1",
+ "eslint-plugin-node": "~9.1.0",
+ "eslint-plugin-prettier": "~3.1.0",
+ "eslint-plugin-promise": "~4.2.1",
+ "eslint-plugin-standard": "~4.0.0",
+ "graphql-request": "~1.8.2",
+ "jest": "~24.8.0",
+ "nodemon": "~1.19.1",
+ "prettier": "~1.18.2",
+ "supertest": "~4.0.2"
+ }
+}
diff --git a/Human-Connection/backend/public/.gitkeep b/Human-Connection/backend/public/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/backend/public/img/badges/fundraisingbox_de_airship.svg b/Human-Connection/backend/public/img/badges/fundraisingbox_de_airship.svg
new file mode 100644
index 000000000..078dcf4f9
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/fundraisingbox_de_airship.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/fundraisingbox_de_alienship.svg b/Human-Connection/backend/public/img/badges/fundraisingbox_de_alienship.svg
new file mode 100644
index 000000000..e891c5fa9
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/fundraisingbox_de_alienship.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/fundraisingbox_de_balloon.svg b/Human-Connection/backend/public/img/badges/fundraisingbox_de_balloon.svg
new file mode 100644
index 000000000..6fc436d86
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/fundraisingbox_de_balloon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/fundraisingbox_de_bigballoon.svg b/Human-Connection/backend/public/img/badges/fundraisingbox_de_bigballoon.svg
new file mode 100644
index 000000000..e2650963a
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/fundraisingbox_de_bigballoon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/fundraisingbox_de_crane.svg b/Human-Connection/backend/public/img/badges/fundraisingbox_de_crane.svg
new file mode 100644
index 000000000..4904c5ec5
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/fundraisingbox_de_crane.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/fundraisingbox_de_glider.svg b/Human-Connection/backend/public/img/badges/fundraisingbox_de_glider.svg
new file mode 100644
index 000000000..0c15955de
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/fundraisingbox_de_glider.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/fundraisingbox_de_helicopter.svg b/Human-Connection/backend/public/img/badges/fundraisingbox_de_helicopter.svg
new file mode 100644
index 000000000..3a84e4466
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/fundraisingbox_de_helicopter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/fundraisingbox_de_starter.svg b/Human-Connection/backend/public/img/badges/fundraisingbox_de_starter.svg
new file mode 100644
index 000000000..99980560e
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/fundraisingbox_de_starter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/indiegogo_en_bear.svg b/Human-Connection/backend/public/img/badges/indiegogo_en_bear.svg
new file mode 100644
index 000000000..43465a0e6
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/indiegogo_en_bear.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/indiegogo_en_panda.svg b/Human-Connection/backend/public/img/badges/indiegogo_en_panda.svg
new file mode 100644
index 000000000..a2f211e85
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/indiegogo_en_panda.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/indiegogo_en_rabbit.svg b/Human-Connection/backend/public/img/badges/indiegogo_en_rabbit.svg
new file mode 100644
index 000000000..c8c0c9727
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/indiegogo_en_rabbit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/indiegogo_en_racoon.svg b/Human-Connection/backend/public/img/badges/indiegogo_en_racoon.svg
new file mode 100644
index 000000000..619cb75f1
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/indiegogo_en_racoon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/indiegogo_en_rhino.svg b/Human-Connection/backend/public/img/badges/indiegogo_en_rhino.svg
new file mode 100644
index 000000000..71c0eb1ad
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/indiegogo_en_rhino.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/indiegogo_en_tiger.svg b/Human-Connection/backend/public/img/badges/indiegogo_en_tiger.svg
new file mode 100644
index 000000000..88583a472
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/indiegogo_en_tiger.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/indiegogo_en_turtle.svg b/Human-Connection/backend/public/img/badges/indiegogo_en_turtle.svg
new file mode 100644
index 000000000..6b5431c2e
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/indiegogo_en_turtle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/indiegogo_en_whale.svg b/Human-Connection/backend/public/img/badges/indiegogo_en_whale.svg
new file mode 100644
index 000000000..458e03b6d
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/indiegogo_en_whale.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/indiegogo_en_wolf.svg b/Human-Connection/backend/public/img/badges/indiegogo_en_wolf.svg
new file mode 100644
index 000000000..e4952d86f
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/indiegogo_en_wolf.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/user_role_admin.svg b/Human-Connection/backend/public/img/badges/user_role_admin.svg
new file mode 100644
index 000000000..101e7458d
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/user_role_admin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/user_role_developer.svg b/Human-Connection/backend/public/img/badges/user_role_developer.svg
new file mode 100644
index 000000000..55d363c9a
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/user_role_developer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/user_role_moderator.svg b/Human-Connection/backend/public/img/badges/user_role_moderator.svg
new file mode 100644
index 000000000..bb2e5fde6
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/user_role_moderator.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/wooold_de_bee.svg b/Human-Connection/backend/public/img/badges/wooold_de_bee.svg
new file mode 100644
index 000000000..e716c6116
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/wooold_de_bee.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/wooold_de_butterfly.svg b/Human-Connection/backend/public/img/badges/wooold_de_butterfly.svg
new file mode 100644
index 000000000..6d2b83e31
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/wooold_de_butterfly.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/wooold_de_double_rainbow.svg b/Human-Connection/backend/public/img/badges/wooold_de_double_rainbow.svg
new file mode 100644
index 000000000..406001188
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/wooold_de_double_rainbow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/wooold_de_end_of_rainbow.svg b/Human-Connection/backend/public/img/badges/wooold_de_end_of_rainbow.svg
new file mode 100644
index 000000000..2ae24cb7b
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/wooold_de_end_of_rainbow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/wooold_de_flower.svg b/Human-Connection/backend/public/img/badges/wooold_de_flower.svg
new file mode 100644
index 000000000..ffc4b3da4
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/wooold_de_flower.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/wooold_de_lifetree.svg b/Human-Connection/backend/public/img/badges/wooold_de_lifetree.svg
new file mode 100644
index 000000000..5a89fa5f9
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/wooold_de_lifetree.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/wooold_de_magic_rainbow.svg b/Human-Connection/backend/public/img/badges/wooold_de_magic_rainbow.svg
new file mode 100644
index 000000000..74df95190
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/wooold_de_magic_rainbow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/img/badges/wooold_de_super_founder.svg b/Human-Connection/backend/public/img/badges/wooold_de_super_founder.svg
new file mode 100644
index 000000000..b437f6383
--- /dev/null
+++ b/Human-Connection/backend/public/img/badges/wooold_de_super_founder.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Human-Connection/backend/public/uploads/.gitkeep b/Human-Connection/backend/public/uploads/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/backend/src/activitypub/ActivityPub.js b/Human-Connection/backend/src/activitypub/ActivityPub.js
new file mode 100644
index 000000000..12671f330
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/ActivityPub.js
@@ -0,0 +1,236 @@
+import { extractNameFromId, extractDomainFromUrl, signAndSend } from './utils'
+import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity'
+import request from 'request'
+import as from 'activitystrea.ms'
+import NitroDataSource from './NitroDataSource'
+import router from './routes'
+import Collections from './Collections'
+import uuid from 'uuid/v4'
+import CONFIG from '../config'
+const debug = require('debug')('ea')
+
+let activityPub = null
+
+export { activityPub }
+
+export default class ActivityPub {
+ constructor(activityPubEndpointUri, internalGraphQlUri) {
+ this.endpoint = activityPubEndpointUri
+ this.dataSource = new NitroDataSource(internalGraphQlUri)
+ this.collections = new Collections(this.dataSource)
+ }
+
+ static init(server) {
+ if (!activityPub) {
+ activityPub = new ActivityPub(CONFIG.CLIENT_URI, CONFIG.GRAPHQL_URI)
+
+ // integrate into running graphql express server
+ server.express.set('ap', activityPub)
+ server.express.use(router)
+ console.log('-> ActivityPub middleware added to the graphql express server') // eslint-disable-line no-console
+ } else {
+ console.log('-> ActivityPub middleware already added to the graphql express server') // eslint-disable-line no-console
+ }
+ }
+
+ handleFollowActivity(activity) {
+ debug(`inside FOLLOW ${activity.actor}`)
+ let toActorName = extractNameFromId(activity.object)
+ let fromDomain = extractDomainFromUrl(activity.actor)
+ const dataSource = this.dataSource
+
+ return new Promise((resolve, reject) => {
+ request(
+ {
+ url: activity.actor,
+ headers: {
+ Accept: 'application/activity+json',
+ },
+ },
+ async (err, response, toActorObject) => {
+ if (err) return reject(err)
+ // save shared inbox
+ toActorObject = JSON.parse(toActorObject)
+ await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
+
+ let followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
+ activity.object,
+ )
+
+ const followActivity = as
+ .follow()
+ .id(activity.id)
+ .actor(activity.actor)
+ .object(activity.object)
+
+ // add follower if not already in collection
+ if (followersCollectionPage.orderedItems.includes(activity.actor)) {
+ debug('follower already in collection!')
+ debug(`inbox = ${toActorObject.inbox}`)
+ resolve(
+ sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
+ )
+ } else {
+ followersCollectionPage.orderedItems.push(activity.actor)
+ }
+ debug(`toActorObject = ${toActorObject}`)
+ toActorObject =
+ typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
+ debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
+ debug(`inbox = ${toActorObject.inbox}`)
+ debug(`outbox = ${toActorObject.outbox}`)
+ debug(`followers = ${toActorObject.followers}`)
+ debug(`following = ${toActorObject.following}`)
+
+ try {
+ await dataSource.saveFollowersCollectionPage(followersCollectionPage)
+ debug('follow activity saved')
+ resolve(
+ sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
+ )
+ } catch (e) {
+ debug('followers update error!', e)
+ resolve(
+ sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
+ )
+ }
+ },
+ )
+ })
+ }
+
+ handleUndoActivity(activity) {
+ debug('inside UNDO')
+ switch (activity.object.type) {
+ case 'Follow':
+ const followActivity = activity.object
+ return this.dataSource.undoFollowActivity(followActivity.actor, followActivity.object)
+ case 'Like':
+ return this.dataSource.deleteShouted(activity)
+ default:
+ }
+ }
+
+ handleCreateActivity(activity) {
+ debug('inside create')
+ switch (activity.object.type) {
+ case 'Article':
+ case 'Note':
+ const articleObject = activity.object
+ if (articleObject.inReplyTo) {
+ return this.dataSource.createComment(activity)
+ } else {
+ return this.dataSource.createPost(activity)
+ }
+ default:
+ }
+ }
+
+ handleDeleteActivity(activity) {
+ debug('inside delete')
+ switch (activity.object.type) {
+ case 'Article':
+ case 'Note':
+ return this.dataSource.deletePost(activity)
+ default:
+ }
+ }
+
+ handleUpdateActivity(activity) {
+ debug('inside update')
+ switch (activity.object.type) {
+ case 'Note':
+ case 'Article':
+ return this.dataSource.updatePost(activity)
+ default:
+ }
+ }
+
+ handleLikeActivity(activity) {
+ // TODO differ if activity is an Article/Note/etc.
+ return this.dataSource.createShouted(activity)
+ }
+
+ handleDislikeActivity(activity) {
+ // TODO differ if activity is an Article/Note/etc.
+ return this.dataSource.deleteShouted(activity)
+ }
+
+ async handleAcceptActivity(activity) {
+ debug('inside accept')
+ switch (activity.object.type) {
+ case 'Follow':
+ const followObject = activity.object
+ const followingCollectionPage = await this.collections.getFollowingCollectionPage(
+ followObject.actor,
+ )
+ followingCollectionPage.orderedItems.push(followObject.object)
+ await this.dataSource.saveFollowingCollectionPage(followingCollectionPage)
+ }
+ }
+
+ getActorObject(url) {
+ return new Promise((resolve, reject) => {
+ request(
+ {
+ url: url,
+ headers: {
+ Accept: 'application/json',
+ },
+ },
+ (err, response, body) => {
+ if (err) {
+ reject(err)
+ }
+ resolve(JSON.parse(body))
+ },
+ )
+ })
+ }
+
+ generateStatusId(slug) {
+ return `https://${this.host}/activitypub/users/${slug}/status/${uuid()}`
+ }
+
+ async sendActivity(activity) {
+ delete activity.send
+ const fromName = extractNameFromId(activity.actor)
+ if (Array.isArray(activity.to) && isPublicAddressed(activity)) {
+ debug('is public addressed')
+ const sharedInboxEndpoints = await this.dataSource.getSharedInboxEndpoints()
+ // serve shared inbox endpoints
+ sharedInboxEndpoints.map(sharedInbox => {
+ return this.trySend(activity, fromName, new URL(sharedInbox).host, sharedInbox)
+ })
+ activity.to = activity.to.filter(recipient => {
+ return !isPublicAddressed({ to: recipient })
+ })
+ // serve the rest
+ 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)
+ })
+ } else if (typeof activity.to === 'string') {
+ debug('is string')
+ 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 => {
+ const actorObject = await this.getActorObject(recipient)
+ return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox)
+ })
+ }
+ }
+ async trySend(activity, fromName, host, url, tries = 5) {
+ try {
+ return await signAndSend(activity, fromName, host, url)
+ } catch (e) {
+ if (tries > 0) {
+ setTimeout(function() {
+ return this.trySend(activity, fromName, host, url, --tries)
+ }, 20000)
+ }
+ }
+ }
+}
diff --git a/Human-Connection/backend/src/activitypub/Collections.js b/Human-Connection/backend/src/activitypub/Collections.js
new file mode 100644
index 000000000..641db596a
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/Collections.js
@@ -0,0 +1,28 @@
+export default class Collections {
+ constructor(dataSource) {
+ this.dataSource = dataSource
+ }
+ getFollowersCollection(actorId) {
+ return this.dataSource.getFollowersCollection(actorId)
+ }
+
+ getFollowersCollectionPage(actorId) {
+ return this.dataSource.getFollowersCollectionPage(actorId)
+ }
+
+ getFollowingCollection(actorId) {
+ return this.dataSource.getFollowingCollection(actorId)
+ }
+
+ getFollowingCollectionPage(actorId) {
+ return this.dataSource.getFollowingCollectionPage(actorId)
+ }
+
+ getOutboxCollection(actorId) {
+ return this.dataSource.getOutboxCollection(actorId)
+ }
+
+ getOutboxCollectionPage(actorId) {
+ return this.dataSource.getOutboxCollectionPage(actorId)
+ }
+}
diff --git a/Human-Connection/backend/src/activitypub/NitroDataSource.js b/Human-Connection/backend/src/activitypub/NitroDataSource.js
new file mode 100644
index 000000000..0900bed6c
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/NitroDataSource.js
@@ -0,0 +1,579 @@
+import {
+ throwErrorIfApolloErrorOccurred,
+ extractIdFromActivityId,
+ extractNameFromId,
+ constructIdFromName,
+} from './utils'
+import { createOrderedCollection, createOrderedCollectionPage } from './utils/collection'
+import { createArticleObject, isPublicAddressed } from './utils/activity'
+import crypto from 'crypto'
+import gql from 'graphql-tag'
+import { createHttpLink } from 'apollo-link-http'
+import { setContext } from 'apollo-link-context'
+import { InMemoryCache } from 'apollo-cache-inmemory'
+import fetch from 'node-fetch'
+import { ApolloClient } from 'apollo-client'
+import trunc from 'trunc-html'
+const debug = require('debug')('ea:nitro-datasource')
+
+export default class NitroDataSource {
+ constructor(uri) {
+ this.uri = uri
+ const defaultOptions = {
+ query: {
+ fetchPolicy: 'network-only',
+ errorPolicy: 'all',
+ },
+ }
+ const link = createHttpLink({ uri: this.uri, fetch: fetch }) // eslint-disable-line
+ const cache = new InMemoryCache()
+ const authLink = setContext((_, { headers }) => {
+ // generate the authentication token (maybe from env? Which user?)
+ const token =
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoiUGV0ZXIgTHVzdGlnIiwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9qb2huY2FmYXp6YS8xMjguanBnIiwiaWQiOiJ1MSIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5vcmciLCJzbHVnIjoicGV0ZXItbHVzdGlnIiwiaWF0IjoxNTUyNDIwMTExLCJleHAiOjE2Mzg4MjAxMTEsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUxIn0.G7An1yeQUViJs-0Qj-Tc-zm0WrLCMB3M02pfPnm6xzw'
+ // return the headers to the context so httpLink can read them
+ return {
+ headers: {
+ ...headers,
+ Authorization: token ? `Bearer ${token}` : '',
+ },
+ }
+ })
+ this.client = new ApolloClient({
+ link: authLink.concat(link),
+ cache: cache,
+ defaultOptions,
+ })
+ }
+
+ async getFollowersCollection(actorId) {
+ const slug = extractNameFromId(actorId)
+ debug(`slug= ${slug}`)
+ const result = await this.client.query({
+ query: gql`
+ query {
+ User(slug: "${slug}") {
+ followedByCount
+ }
+ }
+ `,
+ })
+ debug('successfully fetched followers')
+ debug(result.data)
+ if (result.data) {
+ const actor = result.data.User[0]
+ const followersCount = actor.followedByCount
+
+ const followersCollection = createOrderedCollection(slug, 'followers')
+ followersCollection.totalItems = followersCount
+
+ return followersCollection
+ } else {
+ throwErrorIfApolloErrorOccurred(result)
+ }
+ }
+
+ async getFollowersCollectionPage(actorId) {
+ const slug = extractNameFromId(actorId)
+ debug(`getFollowersPage slug = ${slug}`)
+ const result = await this.client.query({
+ query: gql`
+ query {
+ User(slug:"${slug}") {
+ followedBy {
+ slug
+ }
+ followedByCount
+ }
+ }
+ `,
+ })
+
+ debug(result.data)
+ if (result.data) {
+ const actor = result.data.User[0]
+ const followers = actor.followedBy
+ const followersCount = actor.followedByCount
+
+ const followersCollection = createOrderedCollectionPage(slug, 'followers')
+ followersCollection.totalItems = followersCount
+ debug(`followers = ${JSON.stringify(followers, null, 2)}`)
+ await Promise.all(
+ followers.map(async follower => {
+ followersCollection.orderedItems.push(constructIdFromName(follower.slug))
+ }),
+ )
+
+ return followersCollection
+ } else {
+ throwErrorIfApolloErrorOccurred(result)
+ }
+ }
+
+ async getFollowingCollection(actorId) {
+ const slug = extractNameFromId(actorId)
+ const result = await this.client.query({
+ query: gql`
+ query {
+ User(slug:"${slug}") {
+ followingCount
+ }
+ }
+ `,
+ })
+
+ debug(result.data)
+ if (result.data) {
+ const actor = result.data.User[0]
+ const followingCount = actor.followingCount
+
+ const followingCollection = createOrderedCollection(slug, 'following')
+ followingCollection.totalItems = followingCount
+
+ return followingCollection
+ } else {
+ throwErrorIfApolloErrorOccurred(result)
+ }
+ }
+
+ async getFollowingCollectionPage(actorId) {
+ const slug = extractNameFromId(actorId)
+ const result = await this.client.query({
+ query: gql`
+ query {
+ User(slug:"${slug}") {
+ following {
+ slug
+ }
+ followingCount
+ }
+ }
+ `,
+ })
+
+ debug(result.data)
+ if (result.data) {
+ const actor = result.data.User[0]
+ const following = actor.following
+ const followingCount = actor.followingCount
+
+ const followingCollection = createOrderedCollectionPage(slug, 'following')
+ followingCollection.totalItems = followingCount
+
+ await Promise.all(
+ following.map(async user => {
+ followingCollection.orderedItems.push(await constructIdFromName(user.slug))
+ }),
+ )
+
+ return followingCollection
+ } else {
+ throwErrorIfApolloErrorOccurred(result)
+ }
+ }
+
+ async getOutboxCollection(actorId) {
+ const slug = extractNameFromId(actorId)
+ const result = await this.client.query({
+ query: gql`
+ query {
+ User(slug:"${slug}") {
+ contributions {
+ title
+ slug
+ content
+ contentExcerpt
+ createdAt
+ }
+ }
+ }
+ `,
+ })
+
+ debug(result.data)
+ if (result.data) {
+ const actor = result.data.User[0]
+ const posts = actor.contributions
+
+ const outboxCollection = createOrderedCollection(slug, 'outbox')
+ outboxCollection.totalItems = posts.length
+
+ return outboxCollection
+ } else {
+ throwErrorIfApolloErrorOccurred(result)
+ }
+ }
+
+ async getOutboxCollectionPage(actorId) {
+ const slug = extractNameFromId(actorId)
+ debug(`inside getting outbox collection page => ${slug}`)
+ const result = await this.client.query({
+ query: gql`
+ query {
+ User(slug:"${slug}") {
+ actorId
+ contributions {
+ id
+ activityId
+ objectId
+ title
+ slug
+ content
+ contentExcerpt
+ createdAt
+ author {
+ slug
+ }
+ }
+ }
+ }
+ `,
+ })
+
+ debug(result.data)
+ if (result.data) {
+ const actor = result.data.User[0]
+ const posts = actor.contributions
+
+ const outboxCollection = createOrderedCollectionPage(slug, 'outbox')
+ outboxCollection.totalItems = posts.length
+ await Promise.all(
+ posts.map(async post => {
+ outboxCollection.orderedItems.push(
+ await createArticleObject(
+ post.activityId,
+ post.objectId,
+ post.content,
+ post.author.slug,
+ post.id,
+ post.createdAt,
+ ),
+ )
+ }),
+ )
+
+ debug('after createNote')
+ return outboxCollection
+ } else {
+ throwErrorIfApolloErrorOccurred(result)
+ }
+ }
+
+ async undoFollowActivity(fromActorId, toActorId) {
+ const fromUserId = await this.ensureUser(fromActorId)
+ const toUserId = await this.ensureUser(toActorId)
+ const result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ RemoveUserFollowedBy(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) {
+ from { name }
+ }
+ }
+ `,
+ })
+ debug(`undoFollowActivity result = ${JSON.stringify(result, null, 2)}`)
+ throwErrorIfApolloErrorOccurred(result)
+ }
+
+ async saveFollowersCollectionPage(followersCollection, onlyNewestItem = true) {
+ debug('inside saveFollowers')
+ let orderedItems = followersCollection.orderedItems
+ const toUserName = extractNameFromId(followersCollection.id)
+ const toUserId = await this.ensureUser(constructIdFromName(toUserName))
+ orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems
+
+ return Promise.all(
+ orderedItems.map(async follower => {
+ debug(`follower = ${follower}`)
+ const fromUserId = await this.ensureUser(follower)
+ debug(`fromUserId = ${fromUserId}`)
+ debug(`toUserId = ${toUserId}`)
+ const result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ AddUserFollowedBy(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) {
+ from { name }
+ }
+ }
+ `,
+ })
+ debug(`addUserFollowedBy edge = ${JSON.stringify(result, null, 2)}`)
+ throwErrorIfApolloErrorOccurred(result)
+ debug('saveFollowers: added follow edge successfully')
+ }),
+ )
+ }
+ async saveFollowingCollectionPage(followingCollection, onlyNewestItem = true) {
+ debug('inside saveFollowers')
+ let orderedItems = followingCollection.orderedItems
+ const fromUserName = extractNameFromId(followingCollection.id)
+ const fromUserId = await this.ensureUser(constructIdFromName(fromUserName))
+ orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems
+ return Promise.all(
+ orderedItems.map(async following => {
+ debug(`follower = ${following}`)
+ const toUserId = await this.ensureUser(following)
+ debug(`fromUserId = ${fromUserId}`)
+ debug(`toUserId = ${toUserId}`)
+ const result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ AddUserFollowing(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) {
+ from { name }
+ }
+ }
+ `,
+ })
+ debug(`addUserFollowing edge = ${JSON.stringify(result, null, 2)}`)
+ throwErrorIfApolloErrorOccurred(result)
+ debug('saveFollowing: added follow edge successfully')
+ }),
+ )
+ }
+
+ async createPost(activity) {
+ // TODO how to handle the to field? Now the post is just created, doesn't matter who is the recipient
+ // createPost
+ const postObject = activity.object
+ if (!isPublicAddressed(postObject)) {
+ return debug(
+ 'createPost: not send to public (sending to specific persons is not implemented yet)',
+ )
+ }
+ const title = postObject.summary
+ ? postObject.summary
+ : postObject.content
+ .split(' ')
+ .slice(0, 5)
+ .join(' ')
+ const postId = extractIdFromActivityId(postObject.id)
+ debug('inside create post')
+ let result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ CreatePost(content: "${postObject.content}", contentExcerpt: "${trunc(
+ postObject.content,
+ 120,
+ )}", title: "${title}", id: "${postId}", objectId: "${postObject.id}", activityId: "${
+ activity.id
+ }") {
+ id
+ }
+ }
+ `,
+ })
+
+ throwErrorIfApolloErrorOccurred(result)
+
+ // ensure user and add author to post
+ const userId = await this.ensureUser(postObject.attributedTo)
+ debug(`userId = ${userId}`)
+ debug(`postId = ${postId}`)
+ result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ AddPostAuthor(from: {id: "${userId}"}, to: {id: "${postId}"}) {
+ from {
+ name
+ }
+ }
+ }
+ `,
+ })
+
+ throwErrorIfApolloErrorOccurred(result)
+ }
+
+ async deletePost(activity) {
+ const result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ DeletePost(id: "${extractIdFromActivityId(activity.object.id)}") {
+ title
+ }
+ }
+ `,
+ })
+ throwErrorIfApolloErrorOccurred(result)
+ }
+
+ async updatePost(activity) {
+ const postObject = activity.object
+ const postId = extractIdFromActivityId(postObject.id)
+ const date = postObject.updated ? postObject.updated : new Date().toISOString()
+ const result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ UpdatePost(content: "${postObject.content}", contentExcerpt: "${
+ trunc(postObject.content, 120).html
+ }", id: "${postId}", updatedAt: "${date}") {
+ title
+ }
+ }
+ `,
+ })
+ throwErrorIfApolloErrorOccurred(result)
+ }
+
+ async createShouted(activity) {
+ const userId = await this.ensureUser(activity.actor)
+ const postId = extractIdFromActivityId(activity.object)
+ const result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ AddUserShouted(from: {id: "${userId}"}, to: {id: "${postId}"}) {
+ from {
+ name
+ }
+ }
+ }
+ `,
+ })
+ throwErrorIfApolloErrorOccurred(result)
+ if (!result.data.AddUserShouted) {
+ debug('something went wrong shouting post')
+ throw Error('User or Post not exists')
+ }
+ }
+
+ async deleteShouted(activity) {
+ const userId = await this.ensureUser(activity.actor)
+ const postId = extractIdFromActivityId(activity.object)
+ const result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ RemoveUserShouted(from: {id: "${userId}"}, to: {id: "${postId}"}) {
+ from {
+ name
+ }
+ }
+ }
+ `,
+ })
+ throwErrorIfApolloErrorOccurred(result)
+ if (!result.data.AddUserShouted) {
+ debug('something went wrong disliking a post')
+ throw Error('User or Post not exists')
+ }
+ }
+
+ async getSharedInboxEndpoints() {
+ const result = await this.client.query({
+ query: gql`
+ query {
+ SharedInboxEndpoint {
+ uri
+ }
+ }
+ `,
+ })
+ throwErrorIfApolloErrorOccurred(result)
+ return result.data.SharedInboxEnpoint
+ }
+ async addSharedInboxEndpoint(uri) {
+ try {
+ const result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ CreateSharedInboxEndpoint(uri: "${uri}")
+ }
+ `,
+ })
+ throwErrorIfApolloErrorOccurred(result)
+ return true
+ } catch (e) {
+ return false
+ }
+ }
+
+ async createComment(activity) {
+ const postObject = activity.object
+ let result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ CreateComment(content: "${
+ postObject.content
+ }", activityId: "${extractIdFromActivityId(activity.id)}") {
+ id
+ }
+ }
+ `,
+ })
+ throwErrorIfApolloErrorOccurred(result)
+
+ const toUserId = await this.ensureUser(activity.actor)
+ const result2 = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ AddCommentAuthor(from: {id: "${result.data.CreateComment.id}"}, to: {id: "${toUserId}"}) {
+ id
+ }
+ }
+ `,
+ })
+ throwErrorIfApolloErrorOccurred(result2)
+
+ const postId = extractIdFromActivityId(postObject.inReplyTo)
+ result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ AddCommentPost(from: { id: "${result.data.CreateComment.id}", to: { id: "${postId}" }}) {
+ id
+ }
+ }
+ `,
+ })
+
+ throwErrorIfApolloErrorOccurred(result)
+ }
+
+ /**
+ * This function will search for user existence and will create a disabled user with a random 16 bytes password when no user is found.
+ *
+ * @param actorId
+ * @returns {Promise<*>}
+ */
+ async ensureUser(actorId) {
+ debug(`inside ensureUser = ${actorId}`)
+ const name = extractNameFromId(actorId)
+ const queryResult = await this.client.query({
+ query: gql`
+ query {
+ User(slug: "${name}") {
+ id
+ }
+ }
+ `,
+ })
+
+ if (
+ queryResult.data &&
+ Array.isArray(queryResult.data.User) &&
+ queryResult.data.User.length > 0
+ ) {
+ debug('ensureUser: user exists.. return id')
+ // user already exists.. return the id
+ return queryResult.data.User[0].id
+ } else {
+ 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 result = await this.client.mutate({
+ mutation: gql`
+ mutation {
+ CreateUser(password: "${pw}", slug:"${slug}", actorId: "${actorId}", name: "${name}", email: "${slug}@test.org") {
+ id
+ }
+ }
+ `,
+ })
+ throwErrorIfApolloErrorOccurred(result)
+
+ return result.data.CreateUser.id
+ }
+ }
+}
diff --git a/Human-Connection/backend/src/activitypub/routes/inbox.js b/Human-Connection/backend/src/activitypub/routes/inbox.js
new file mode 100644
index 000000000..b31b89ed4
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/routes/inbox.js
@@ -0,0 +1,54 @@
+import express from 'express'
+import { activityPub } from '../ActivityPub'
+
+const debug = require('debug')('ea:inbox')
+
+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) {
+ debug(`Content-Type = ${req.get('Content-Type')}`)
+ debug(`body = ${JSON.stringify(req.body, null, 2)}`)
+ debug(`Request headers = ${JSON.stringify(req.headers, null, 2)}`)
+ switch (req.body.type) {
+ case 'Create':
+ await activityPub.handleCreateActivity(req.body).catch(next)
+ break
+ case 'Undo':
+ await activityPub.handleUndoActivity(req.body).catch(next)
+ break
+ case 'Follow':
+ await activityPub.handleFollowActivity(req.body).catch(next)
+ break
+ case 'Delete':
+ await activityPub.handleDeleteActivity(req.body).catch(next)
+ break
+ /* eslint-disable */
+ case 'Update':
+ await activityPub.handleUpdateActivity(req.body).catch(next)
+ break
+ case 'Accept':
+ await activityPub.handleAcceptActivity(req.body).catch(next)
+ case 'Reject':
+ // Do nothing
+ break
+ case 'Add':
+ break
+ case 'Remove':
+ break
+ case 'Like':
+ await activityPub.handleLikeActivity(req.body).catch(next)
+ break
+ case 'Dislike':
+ await activityPub.handleDislikeActivity(req.body).catch(next)
+ break
+ case 'Announce':
+ debug('else!!')
+ debug(JSON.stringify(req.body, null, 2))
+ }
+ /* eslint-enable */
+ res.status(200).end()
+})
+
+export default router
diff --git a/Human-Connection/backend/src/activitypub/routes/index.js b/Human-Connection/backend/src/activitypub/routes/index.js
new file mode 100644
index 000000000..c7d31f1c4
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/routes/index.js
@@ -0,0 +1,27 @@
+import user from './user'
+import inbox from './inbox'
+import webFinger from './webFinger'
+import express from 'express'
+import cors from 'cors'
+import verify from './verify'
+
+const router = express.Router()
+
+router.use('/.well-known/webFinger', cors(), express.urlencoded({ extended: true }), webFinger)
+router.use(
+ '/activitypub/users',
+ cors(),
+ express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
+ express.urlencoded({ extended: true }),
+ user,
+)
+router.use(
+ '/activitypub/inbox',
+ cors(),
+ express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
+ express.urlencoded({ extended: true }),
+ verify,
+ inbox,
+)
+
+export default router
diff --git a/Human-Connection/backend/src/activitypub/routes/serveUser.js b/Human-Connection/backend/src/activitypub/routes/serveUser.js
new file mode 100644
index 000000000..6f4472235
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/routes/serveUser.js
@@ -0,0 +1,54 @@
+import { createActor } from '../utils/actor'
+const gql = require('graphql-tag')
+const debug = require('debug')('ea:serveUser')
+
+export async function serveUser(req, res, next) {
+ let name = req.params.name
+
+ if (name.startsWith('@')) {
+ name = name.slice(1)
+ }
+
+ debug(`name = ${name}`)
+ const result = await req.app
+ .get('ap')
+ .dataSource.client.query({
+ query: gql`
+ query {
+ User(slug: "${name}") {
+ publicKey
+ }
+ }
+ `,
+ })
+ .catch(reason => {
+ debug(`serveUser User fetch error: ${reason}`)
+ })
+
+ if (result.data && Array.isArray(result.data.User) && result.data.User.length > 0) {
+ const publicKey = result.data.User[0].publicKey
+ const actor = createActor(name, publicKey)
+ debug(`actor = ${JSON.stringify(actor, null, 2)}`)
+ debug(
+ `accepts json = ${req.accepts([
+ 'application/activity+json',
+ 'application/ld+json',
+ 'application/json',
+ ])}`,
+ )
+ if (req.accepts(['application/activity+json', 'application/ld+json', 'application/json'])) {
+ return res.json(actor)
+ } else if (req.accepts('text/html')) {
+ // TODO show user's profile page instead of the actor object
+ /* const outbox = JSON.parse(result.outbox)
+ const posts = outbox.orderedItems.filter((el) => { return el.object.type === 'Note'})
+ const actor = result.actor
+ debug(posts) */
+ // res.render('user', { user: actor, posts: JSON.stringify(posts)})
+ return res.json(actor)
+ }
+ } else {
+ debug(`error getting publicKey for actor ${name}`)
+ next()
+ }
+}
diff --git a/Human-Connection/backend/src/activitypub/routes/user.js b/Human-Connection/backend/src/activitypub/routes/user.js
new file mode 100644
index 000000000..9dc9b5071
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/routes/user.js
@@ -0,0 +1,92 @@
+import { sendCollection } from '../utils/collection'
+import express from 'express'
+import { serveUser } from './serveUser'
+import { activityPub } from '../ActivityPub'
+import verify from './verify'
+
+const router = express.Router()
+const debug = require('debug')('ea:user')
+
+router.get('/:name', async function(req, res, next) {
+ debug('inside user.js -> serveUser')
+ await serveUser(req, res, next)
+})
+
+router.get('/:name/following', (req, res) => {
+ debug('inside user.js -> serveFollowingCollection')
+ const name = req.params.name
+ if (!name) {
+ res.status(400).send('Bad request! Please specify a name.')
+ } else {
+ const collectionName = req.query.page ? 'followingPage' : 'following'
+ sendCollection(collectionName, req, res)
+ }
+})
+
+router.get('/:name/followers', (req, res) => {
+ debug('inside user.js -> serveFollowersCollection')
+ const name = req.params.name
+ if (!name) {
+ return res.status(400).send('Bad request! Please specify a name.')
+ } else {
+ const collectionName = req.query.page ? 'followersPage' : 'followers'
+ sendCollection(collectionName, req, res)
+ }
+})
+
+router.get('/:name/outbox', (req, res) => {
+ debug('inside user.js -> serveOutboxCollection')
+ const name = req.params.name
+ if (!name) {
+ return res.status(400).send('Bad request! Please specify a name.')
+ } else {
+ const collectionName = req.query.page ? 'outboxPage' : 'outbox'
+ sendCollection(collectionName, req, res)
+ }
+})
+
+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)
+ switch (req.body.type) {
+ case 'Create':
+ await activityPub.handleCreateActivity(req.body).catch(next)
+ break
+ case 'Undo':
+ await activityPub.handleUndoActivity(req.body).catch(next)
+ break
+ case 'Follow':
+ await activityPub.handleFollowActivity(req.body).catch(next)
+ break
+ case 'Delete':
+ await activityPub.handleDeleteActivity(req.body).catch(next)
+ break
+ /* eslint-disable */
+ case 'Update':
+ await activityPub.handleUpdateActivity(req.body).catch(next)
+ break
+ case 'Accept':
+ await activityPub.handleAcceptActivity(req.body).catch(next)
+ case 'Reject':
+ // Do nothing
+ break
+ case 'Add':
+ break
+ case 'Remove':
+ break
+ case 'Like':
+ await activityPub.handleLikeActivity(req.body).catch(next)
+ break
+ case 'Dislike':
+ await activityPub.handleDislikeActivity(req.body).catch(next)
+ break
+ case 'Announce':
+ debug('else!!')
+ debug(JSON.stringify(req.body, null, 2))
+ }
+ /* eslint-enable */
+ res.status(200).end()
+})
+
+export default router
diff --git a/Human-Connection/backend/src/activitypub/routes/verify.js b/Human-Connection/backend/src/activitypub/routes/verify.js
new file mode 100644
index 000000000..33603805f
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/routes/verify.js
@@ -0,0 +1,20 @@
+import { verifySignature } from '../security'
+const debug = require('debug')('ea:verify')
+
+export default async (req, res, next) => {
+ debug(`actorId = ${req.body.actor}`)
+ // TODO stop if signature validation fails
+ if (
+ await verifySignature(
+ `${req.protocol}://${req.hostname}:${req.app.get('port')}${req.originalUrl}`,
+ req.headers,
+ )
+ ) {
+ debug('verify = true')
+ next()
+ } else {
+ // throw Error('Signature validation failed!')
+ debug('verify = false')
+ next()
+ }
+}
diff --git a/Human-Connection/backend/src/activitypub/routes/webFinger.js b/Human-Connection/backend/src/activitypub/routes/webFinger.js
new file mode 100644
index 000000000..7d52c69cd
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/routes/webFinger.js
@@ -0,0 +1,43 @@
+import express from 'express'
+import { createWebFinger } from '../utils/actor'
+import gql from 'graphql-tag'
+
+const router = express.Router()
+
+router.get('/', async function(req, res) {
+ const resource = req.query.resource
+ if (!resource || !resource.includes('acct:')) {
+ return res
+ .status(400)
+ .send(
+ 'Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.',
+ )
+ } else {
+ const nameAndDomain = resource.replace('acct:', '')
+ const name = nameAndDomain.split('@')[0]
+
+ let result
+ try {
+ result = await req.app.get('ap').dataSource.client.query({
+ query: gql`
+ query {
+ User(slug: "${name}") {
+ slug
+ }
+ }
+ `,
+ })
+ } catch (error) {
+ return res.status(500).json({ error })
+ }
+
+ if (result.data && result.data.User.length > 0) {
+ const webFinger = createWebFinger(name)
+ return res.contentType('application/jrd+json').json(webFinger)
+ } else {
+ return res.status(404).json({ error: `No record found for ${nameAndDomain}.` })
+ }
+ }
+})
+
+export default router
diff --git a/Human-Connection/backend/src/activitypub/security/httpSignature.spec.js b/Human-Connection/backend/src/activitypub/security/httpSignature.spec.js
new file mode 100644
index 000000000..0c6fbb8b5
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/security/httpSignature.spec.js
@@ -0,0 +1,104 @@
+import { generateRsaKeyPair, createSignature, verifySignature } from '.'
+import crypto from 'crypto'
+import request from 'request'
+jest.mock('request')
+
+let privateKey
+let publicKey
+let headers
+const passphrase = 'a7dsf78sadg87ad87sfagsadg78'
+
+describe('activityPub/security', () => {
+ beforeEach(() => {
+ const pair = generateRsaKeyPair({ passphrase })
+ privateKey = pair.privateKey
+ publicKey = pair.publicKey
+ headers = {
+ Date: '2019-03-08T14:35:45.759Z',
+ Host: 'democracy-app.de',
+ 'Content-Type': 'application/json',
+ }
+ })
+
+ describe('createSignature', () => {
+ describe('returned http signature', () => {
+ let signatureB64
+ let httpSignature
+
+ beforeEach(() => {
+ const signer = crypto.createSign('rsa-sha256')
+ signer.update(
+ '(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json',
+ )
+ signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64')
+ httpSignature = createSignature({
+ privateKey,
+ keyId: 'https://human-connection.org/activitypub/users/lea#main-key',
+ url: 'https://democracy-app.de/activitypub/users/max/inbox',
+ headers,
+ passphrase,
+ })
+ })
+
+ it('contains keyId', () => {
+ expect(httpSignature).toContain(
+ 'keyId="https://human-connection.org/activitypub/users/lea#main-key"',
+ )
+ })
+
+ it('contains default algorithm "rsa-sha256"', () => {
+ expect(httpSignature).toContain('algorithm="rsa-sha256"')
+ })
+
+ it('contains headers', () => {
+ expect(httpSignature).toContain('headers="(request-target) date host content-type"')
+ })
+
+ it('contains signature', () => {
+ expect(httpSignature).toContain('signature="' + signatureB64 + '"')
+ })
+ })
+ })
+
+ describe('verifySignature', () => {
+ let httpSignature
+
+ beforeEach(() => {
+ httpSignature = createSignature({
+ privateKey,
+ keyId: 'http://localhost:4001/activitypub/users/test-user#main-key',
+ url: 'https://democracy-app.de/activitypub/users/max/inbox',
+ headers,
+ passphrase,
+ })
+ const body = {
+ publicKey: {
+ id: 'https://localhost:4001/activitypub/users/test-user#main-key',
+ owner: 'https://localhost:4001/activitypub/users/test-user',
+ publicKeyPem: publicKey,
+ },
+ }
+
+ const mockedRequest = jest.fn((_, callback) => callback(null, null, JSON.stringify(body)))
+ request.mockImplementation(mockedRequest)
+ })
+
+ it('resolves false', async () => {
+ await expect(
+ verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers),
+ ).resolves.toEqual(false)
+ })
+
+ describe('valid signature', () => {
+ beforeEach(() => {
+ headers.Signature = httpSignature
+ })
+
+ it('resolves true', async () => {
+ await expect(
+ verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers),
+ ).resolves.toEqual(true)
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/activitypub/security/index.js b/Human-Connection/backend/src/activitypub/security/index.js
new file mode 100644
index 000000000..9b48b7ed9
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/security/index.js
@@ -0,0 +1,172 @@
+// import dotenv from 'dotenv'
+// import { resolve } from 'path'
+import crypto from 'crypto'
+import request from 'request'
+import CONFIG from './../../config'
+const debug = require('debug')('ea:security')
+
+// TODO Does this reference a local config? Why?
+// dotenv.config({ path: resolve('src', 'activitypub', '.env') })
+
+export function generateRsaKeyPair(options = {}) {
+ const { passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE } = options
+ return crypto.generateKeyPairSync('rsa', {
+ modulusLength: 4096,
+ publicKeyEncoding: {
+ type: 'spki',
+ format: 'pem',
+ },
+ privateKeyEncoding: {
+ type: 'pkcs8',
+ format: 'pem',
+ cipher: 'aes-256-cbc',
+ passphrase,
+ },
+ })
+}
+
+// signing
+export function createSignature(options) {
+ const {
+ privateKey,
+ keyId,
+ url,
+ headers = {},
+ algorithm = 'rsa-sha256',
+ passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE,
+ } = options
+ if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
+ throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`)
+ }
+ const signer = crypto.createSign(algorithm)
+ const signingString = constructSigningString(url, headers)
+ signer.update(signingString)
+ const signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64')
+ const headersString = Object.keys(headers).reduce((result, key) => {
+ return result + ' ' + key.toLowerCase()
+ }, '')
+ return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"`
+}
+
+// verifying
+export function verifySignature(url, headers) {
+ return new Promise((resolve, reject) => {
+ const signatureHeader = headers['signature'] ? headers['signature'] : headers['Signature']
+ if (!signatureHeader) {
+ debug('No Signature header present!')
+ resolve(false)
+ }
+ debug(`Signature Header = ${signatureHeader}`)
+ const signature = extractKeyValueFromSignatureHeader(signatureHeader, 'signature')
+ const algorithm = extractKeyValueFromSignatureHeader(signatureHeader, 'algorithm')
+ const headersString = extractKeyValueFromSignatureHeader(signatureHeader, 'headers')
+ const keyId = extractKeyValueFromSignatureHeader(signatureHeader, 'keyId')
+
+ if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
+ debug('Unsupported hash algorithm specified!')
+ resolve(false)
+ }
+
+ const usedHeaders = headersString.split(' ')
+ const verifyHeaders = {}
+ Object.keys(headers).forEach(key => {
+ if (usedHeaders.includes(key.toLowerCase())) {
+ verifyHeaders[key.toLowerCase()] = headers[key]
+ }
+ })
+ const signingString = constructSigningString(url, verifyHeaders)
+ debug(`keyId= ${keyId}`)
+ request(
+ {
+ url: keyId,
+ headers: {
+ Accept: 'application/json',
+ },
+ },
+ (err, response, body) => {
+ if (err) reject(err)
+ debug(`body = ${body}`)
+ const actor = JSON.parse(body)
+ const publicKeyPem = actor.publicKey.publicKeyPem
+ resolve(httpVerify(publicKeyPem, signature, signingString, algorithm))
+ },
+ )
+ })
+}
+
+// private: signing
+function constructSigningString(url, headers) {
+ const urlObj = new URL(url)
+ let signingString = `(request-target): post ${urlObj.pathname}${
+ urlObj.search !== '' ? urlObj.search : ''
+ }`
+ return Object.keys(headers).reduce((result, key) => {
+ return result + `\n${key.toLowerCase()}: ${headers[key]}`
+ }, signingString)
+}
+
+// private: verifying
+function httpVerify(pubKey, signature, signingString, algorithm) {
+ if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
+ throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`)
+ }
+ const verifier = crypto.createVerify(algorithm)
+ verifier.update(signingString)
+ return verifier.verify(pubKey, signature, 'base64')
+}
+
+// private: verifying
+// 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 => {
+ return !!el.startsWith(key)
+ })[0]
+
+ let firstEqualIndex = keyString.search('=')
+ // When headers are requested add 17 to the index to remove "(request-target) " from the string
+ if (key === 'headers') {
+ firstEqualIndex += 17
+ }
+ return keyString.substring(firstEqualIndex + 2, keyString.length - 1)
+}
+
+// Obtained from invoking crypto.getHashes()
+export const SUPPORTED_HASH_ALGORITHMS = [
+ 'rsa-md4',
+ 'rsa-md5',
+ 'rsa-mdC2',
+ 'rsa-ripemd160',
+ 'rsa-sha1',
+ 'rsa-sha1-2',
+ 'rsa-sha224',
+ 'rsa-sha256',
+ 'rsa-sha384',
+ 'rsa-sha512',
+ 'blake2b512',
+ 'blake2s256',
+ 'md4',
+ 'md4WithRSAEncryption',
+ 'md5',
+ 'md5-sha1',
+ 'md5WithRSAEncryption',
+ 'mdc2',
+ 'mdc2WithRSA',
+ 'ripemd',
+ 'ripemd160',
+ 'ripemd160WithRSA',
+ 'rmd160',
+ 'sha1',
+ 'sha1WithRSAEncryption',
+ 'sha224',
+ 'sha224WithRSAEncryption',
+ 'sha256',
+ 'sha256WithRSAEncryption',
+ 'sha384',
+ 'sha384WithRSAEncryption',
+ 'sha512',
+ 'sha512WithRSAEncryption',
+ 'ssl3-md5',
+ 'ssl3-sha1',
+ 'whirlpool',
+]
diff --git a/Human-Connection/backend/src/activitypub/utils/activity.js b/Human-Connection/backend/src/activitypub/utils/activity.js
new file mode 100644
index 000000000..baf13e1bf
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/utils/activity.js
@@ -0,0 +1,116 @@
+import { activityPub } from '../ActivityPub'
+import { signAndSend, throwErrorIfApolloErrorOccurred } from './index'
+
+import crypto from 'crypto'
+import as from 'activitystrea.ms'
+import gql from 'graphql-tag'
+const debug = require('debug')('ea:utils:activity')
+
+export function createNoteObject(text, name, id, published) {
+ const createUuid = crypto.randomBytes(16).toString('hex')
+
+ return {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: `${activityPub.endpoint}/activitypub/users/${name}/status/${createUuid}`,
+ type: 'Create',
+ actor: `${activityPub.endpoint}/activitypub/users/${name}`,
+ object: {
+ id: `${activityPub.endpoint}/activitypub/users/${name}/status/${id}`,
+ type: 'Note',
+ published: published,
+ attributedTo: `${activityPub.endpoint}/activitypub/users/${name}`,
+ content: text,
+ to: 'https://www.w3.org/ns/activitystreams#Public',
+ },
+ }
+}
+
+export async function createArticleObject(activityId, objectId, text, name, id, published) {
+ const actorId = await getActorId(name)
+
+ return {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: `${activityId}`,
+ type: 'Create',
+ actor: `${actorId}`,
+ object: {
+ id: `${objectId}`,
+ type: 'Article',
+ published: published,
+ attributedTo: `${actorId}`,
+ content: text,
+ to: 'https://www.w3.org/ns/activitystreams#Public',
+ },
+ }
+}
+
+export async function getActorId(name) {
+ const result = await activityPub.dataSource.client.query({
+ query: gql`
+ query {
+ User(slug: "${name}") {
+ actorId
+ }
+ }
+ `,
+ })
+ throwErrorIfApolloErrorOccurred(result)
+ if (Array.isArray(result.data.User) && result.data.User[0]) {
+ return result.data.User[0].actorId
+ } else {
+ throw Error(`No user with name: ${name}`)
+ }
+}
+
+export function sendAcceptActivity(theBody, name, targetDomain, url) {
+ as.accept()
+ .id(
+ `${activityPub.endpoint}/activitypub/users/${name}/status/` +
+ crypto.randomBytes(16).toString('hex'),
+ )
+ .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
+ .object(theBody)
+ .prettyWrite((err, doc) => {
+ if (!err) {
+ return signAndSend(doc, name, targetDomain, url)
+ } else {
+ debug(`error serializing Accept object: ${err}`)
+ throw new Error('error serializing Accept object')
+ }
+ })
+}
+
+export function sendRejectActivity(theBody, name, targetDomain, url) {
+ as.reject()
+ .id(
+ `${activityPub.endpoint}/activitypub/users/${name}/status/` +
+ crypto.randomBytes(16).toString('hex'),
+ )
+ .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
+ .object(theBody)
+ .prettyWrite((err, doc) => {
+ if (!err) {
+ return signAndSend(doc, name, targetDomain, url)
+ } else {
+ debug(`error serializing Accept object: ${err}`)
+ throw new Error('error serializing Accept object')
+ }
+ })
+}
+
+export function isPublicAddressed(postObject) {
+ if (typeof postObject.to === 'string') {
+ postObject.to = [postObject.to]
+ }
+ if (typeof postObject === 'string') {
+ postObject.to = [postObject]
+ }
+ if (Array.isArray(postObject)) {
+ postObject.to = postObject
+ }
+ return (
+ postObject.to.includes('Public') ||
+ postObject.to.includes('as:Public') ||
+ postObject.to.includes('https://www.w3.org/ns/activitystreams#Public')
+ )
+}
diff --git a/Human-Connection/backend/src/activitypub/utils/actor.js b/Human-Connection/backend/src/activitypub/utils/actor.js
new file mode 100644
index 000000000..a08065778
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/utils/actor.js
@@ -0,0 +1,38 @@
+import { activityPub } from '../ActivityPub'
+
+export function createActor(name, pubkey) {
+ return {
+ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'],
+ id: `${activityPub.endpoint}/activitypub/users/${name}`,
+ type: 'Person',
+ preferredUsername: `${name}`,
+ name: `${name}`,
+ following: `${activityPub.endpoint}/activitypub/users/${name}/following`,
+ followers: `${activityPub.endpoint}/activitypub/users/${name}/followers`,
+ inbox: `${activityPub.endpoint}/activitypub/users/${name}/inbox`,
+ outbox: `${activityPub.endpoint}/activitypub/users/${name}/outbox`,
+ url: `${activityPub.endpoint}/activitypub/@${name}`,
+ endpoints: {
+ sharedInbox: `${activityPub.endpoint}/activitypub/inbox`,
+ },
+ publicKey: {
+ id: `${activityPub.endpoint}/activitypub/users/${name}#main-key`,
+ owner: `${activityPub.endpoint}/activitypub/users/${name}`,
+ publicKeyPem: pubkey,
+ },
+ }
+}
+
+export function createWebFinger(name) {
+ const { host } = new URL(activityPub.endpoint)
+ return {
+ subject: `acct:${name}@${host}`,
+ links: [
+ {
+ rel: 'self',
+ type: 'application/activity+json',
+ href: `${activityPub.endpoint}/activitypub/users/${name}`,
+ },
+ ],
+ }
+}
diff --git a/Human-Connection/backend/src/activitypub/utils/collection.js b/Human-Connection/backend/src/activitypub/utils/collection.js
new file mode 100644
index 000000000..29cf69ac2
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/utils/collection.js
@@ -0,0 +1,73 @@
+import { activityPub } from '../ActivityPub'
+import { constructIdFromName } from './index'
+const debug = require('debug')('ea:utils:collections')
+
+export function createOrderedCollection(name, collectionName) {
+ return {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
+ summary: `${name}s ${collectionName} collection`,
+ type: 'OrderedCollection',
+ first: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
+ totalItems: 0,
+ }
+}
+
+export function createOrderedCollectionPage(name, collectionName) {
+ return {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
+ summary: `${name}s ${collectionName} collection`,
+ type: 'OrderedCollectionPage',
+ totalItems: 0,
+ partOf: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
+ orderedItems: [],
+ }
+}
+export function sendCollection(collectionName, req, res) {
+ const name = req.params.name
+ const id = constructIdFromName(name)
+
+ switch (collectionName) {
+ case 'followers':
+ attachThenCatch(activityPub.collections.getFollowersCollection(id), res)
+ break
+
+ case 'followersPage':
+ attachThenCatch(activityPub.collections.getFollowersCollectionPage(id), res)
+ break
+
+ case 'following':
+ attachThenCatch(activityPub.collections.getFollowingCollection(id), res)
+ break
+
+ case 'followingPage':
+ attachThenCatch(activityPub.collections.getFollowingCollectionPage(id), res)
+ break
+
+ case 'outbox':
+ attachThenCatch(activityPub.collections.getOutboxCollection(id), res)
+ break
+
+ case 'outboxPage':
+ attachThenCatch(activityPub.collections.getOutboxCollectionPage(id), res)
+ break
+
+ default:
+ res.status(500).end()
+ }
+}
+
+function attachThenCatch(promise, res) {
+ return promise
+ .then(collection => {
+ res
+ .status(200)
+ .contentType('application/activity+json')
+ .send(collection)
+ })
+ .catch(err => {
+ debug(`error getting a Collection: = ${err}`)
+ res.status(500).end()
+ })
+}
diff --git a/Human-Connection/backend/src/activitypub/utils/index.js b/Human-Connection/backend/src/activitypub/utils/index.js
new file mode 100644
index 000000000..3927f4056
--- /dev/null
+++ b/Human-Connection/backend/src/activitypub/utils/index.js
@@ -0,0 +1,109 @@
+import { activityPub } from '../ActivityPub'
+import gql from 'graphql-tag'
+import { createSignature } from '../security'
+import request from 'request'
+import CONFIG from './../../config'
+const debug = require('debug')('ea:utils')
+
+export function extractNameFromId(uri) {
+ const urlObject = new URL(uri)
+ const pathname = urlObject.pathname
+ const splitted = pathname.split('/')
+
+ return splitted[splitted.indexOf('users') + 1]
+}
+
+export function extractIdFromActivityId(uri) {
+ const urlObject = new URL(uri)
+ const pathname = urlObject.pathname
+ const splitted = pathname.split('/')
+
+ return splitted[splitted.indexOf('status') + 1]
+}
+export function constructIdFromName(name, fromDomain = activityPub.endpoint) {
+ return `${fromDomain}/activitypub/users/${name}`
+}
+
+export function extractDomainFromUrl(url) {
+ return new URL(url).host
+}
+
+export function throwErrorIfApolloErrorOccurred(result) {
+ if (result.error && (result.error.message || result.error.errors)) {
+ throw new Error(
+ `${result.error.message ? result.error.message : result.error.errors[0].message}`,
+ )
+ }
+}
+
+export function signAndSend(activity, fromName, targetDomain, url) {
+ // fix for development: replace with http
+ url = url.indexOf('localhost') > -1 ? url.replace('https', 'http') : url
+ debug(`passhprase = ${CONFIG.PRIVATE_KEY_PASSPHRASE}`)
+ return new Promise(async (resolve, reject) => {
+ debug('inside signAndSend')
+ // get the private key
+ const result = await activityPub.dataSource.client.query({
+ query: gql`
+ query {
+ User(slug: "${fromName}") {
+ privateKey
+ }
+ }
+ `,
+ })
+
+ if (result.error) {
+ reject(result.error)
+ } else {
+ // add security context
+ const parsedActivity = JSON.parse(activity)
+ if (Array.isArray(parsedActivity['@context'])) {
+ parsedActivity['@context'].push('https://w3id.org/security/v1')
+ } else {
+ const context = [parsedActivity['@context']]
+ context.push('https://w3id.org/security/v1')
+ parsedActivity['@context'] = context
+ }
+
+ // deduplicate context strings
+ parsedActivity['@context'] = [...new Set(parsedActivity['@context'])]
+ const privateKey = result.data.User[0].privateKey
+ const date = new Date().toUTCString()
+
+ debug(`url = ${url}`)
+ request(
+ {
+ url: url,
+ headers: {
+ Host: targetDomain,
+ Date: date,
+ Signature: createSignature({
+ privateKey,
+ keyId: `${activityPub.endpoint}/activitypub/users/${fromName}#main-key`,
+ url,
+ headers: {
+ Host: targetDomain,
+ Date: date,
+ 'Content-Type': 'application/activity+json',
+ },
+ }),
+ 'Content-Type': 'application/activity+json',
+ },
+ method: 'POST',
+ body: JSON.stringify(parsedActivity),
+ },
+ (error, response) => {
+ if (error) {
+ debug(`Error = ${JSON.stringify(error, null, 2)}`)
+ reject(error)
+ } else {
+ debug('Response Headers:', JSON.stringify(response.headers, null, 2))
+ debug('Response Body:', JSON.stringify(response.body, null, 2))
+ resolve()
+ }
+ },
+ )
+ }
+ })
+}
diff --git a/Human-Connection/backend/src/bootstrap/directives.js b/Human-Connection/backend/src/bootstrap/directives.js
new file mode 100644
index 000000000..93a7574fb
--- /dev/null
+++ b/Human-Connection/backend/src/bootstrap/directives.js
@@ -0,0 +1,12 @@
+import {
+ GraphQLLowerCaseDirective,
+ GraphQLTrimDirective,
+ GraphQLDefaultToDirective,
+} from 'graphql-custom-directives'
+
+export default function applyDirectives(augmentedSchema) {
+ const directives = [GraphQLLowerCaseDirective, GraphQLTrimDirective, GraphQLDefaultToDirective]
+ augmentedSchema._directives.push.apply(augmentedSchema._directives, directives)
+
+ return augmentedSchema
+}
diff --git a/Human-Connection/backend/src/bootstrap/neo4j.js b/Human-Connection/backend/src/bootstrap/neo4j.js
new file mode 100644
index 000000000..bfa68acf3
--- /dev/null
+++ b/Human-Connection/backend/src/bootstrap/neo4j.js
@@ -0,0 +1,16 @@
+import { v1 as neo4j } from 'neo4j-driver'
+import CONFIG from './../config'
+
+let driver
+
+export function getDriver(options = {}) {
+ const {
+ uri = CONFIG.NEO4J_URI,
+ username = CONFIG.NEO4J_USERNAME,
+ password = CONFIG.NEO4J_PASSWORD,
+ } = options
+ if (!driver) {
+ driver = neo4j.driver(uri, neo4j.auth.basic(username, password))
+ }
+ return driver
+}
diff --git a/Human-Connection/backend/src/bootstrap/scalars.js b/Human-Connection/backend/src/bootstrap/scalars.js
new file mode 100644
index 000000000..eb6d3739b
--- /dev/null
+++ b/Human-Connection/backend/src/bootstrap/scalars.js
@@ -0,0 +1,9 @@
+import { GraphQLDate, GraphQLTime, GraphQLDateTime } from 'graphql-iso-date'
+
+export default function applyScalars(augmentedSchema) {
+ augmentedSchema._typeMap.Date = GraphQLDate
+ augmentedSchema._typeMap.Time = GraphQLTime
+ augmentedSchema._typeMap.DateTime = GraphQLDateTime
+
+ return augmentedSchema
+}
diff --git a/Human-Connection/backend/src/config/index.js b/Human-Connection/backend/src/config/index.js
new file mode 100644
index 000000000..320b636e9
--- /dev/null
+++ b/Human-Connection/backend/src/config/index.js
@@ -0,0 +1,46 @@
+import dotenv from 'dotenv'
+
+dotenv.config()
+
+const {
+ MAPBOX_TOKEN,
+ JWT_SECRET,
+ PRIVATE_KEY_PASSPHRASE,
+ SMTP_IGNORE_TLS = true,
+ SMTP_HOST,
+ SMTP_PORT,
+ SMTP_USERNAME,
+ SMTP_PASSWORD,
+ NEO4J_URI = 'bolt://localhost:7687',
+ NEO4J_USERNAME = 'neo4j',
+ NEO4J_PASSWORD = 'neo4j',
+ GRAPHQL_PORT = 4000,
+ CLIENT_URI = 'http://localhost:3000',
+ GRAPHQL_URI = 'http://localhost:4000',
+} = process.env
+
+export const requiredConfigs = { MAPBOX_TOKEN, JWT_SECRET, PRIVATE_KEY_PASSPHRASE }
+export const smtpConfigs = {
+ SMTP_HOST,
+ SMTP_PORT,
+ SMTP_IGNORE_TLS,
+ SMTP_USERNAME,
+ SMTP_PASSWORD,
+}
+export const neo4jConfigs = { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD }
+export const serverConfigs = { GRAPHQL_PORT, CLIENT_URI, GRAPHQL_URI }
+
+export const developmentConfigs = {
+ DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true',
+ MOCKS: process.env.MOCKS === 'true',
+ DISABLED_MIDDLEWARES:
+ (process.env.NODE_ENV !== 'production' && process.env.DISABLED_MIDDLEWARES) || '',
+}
+
+export default {
+ ...requiredConfigs,
+ ...smtpConfigs,
+ ...neo4jConfigs,
+ ...serverConfigs,
+ ...developmentConfigs,
+}
diff --git a/Human-Connection/backend/src/helpers/asyncForEach.js b/Human-Connection/backend/src/helpers/asyncForEach.js
new file mode 100644
index 000000000..5577cce14
--- /dev/null
+++ b/Human-Connection/backend/src/helpers/asyncForEach.js
@@ -0,0 +1,14 @@
+/**
+ * Provide a way to iterate for each element in an array while waiting for async functions to finish
+ *
+ * @param array
+ * @param callback
+ * @returns {Promise}
+ */
+async function asyncForEach(array, callback) {
+ for (let index = 0; index < array.length; index++) {
+ await callback(array[index], index, array)
+ }
+}
+
+export default asyncForEach
diff --git a/Human-Connection/backend/src/helpers/walkRecursive.js b/Human-Connection/backend/src/helpers/walkRecursive.js
new file mode 100644
index 000000000..db9a4c703
--- /dev/null
+++ b/Human-Connection/backend/src/helpers/walkRecursive.js
@@ -0,0 +1,28 @@
+/**
+ * iterate through all fields and replace it with the callback result
+ * @property data Array
+ * @property fields Array
+ * @property callback Function
+ */
+function walkRecursive(data, fields, callback, _key) {
+ if (!Array.isArray(fields)) {
+ throw new Error('please provide an fields array for the walkRecursive helper')
+ }
+ if (data && typeof data === 'string' && fields.includes(_key)) {
+ // well we found what we searched for, lets replace the value with our callback result
+ data = callback(data, _key)
+ } else if (data && Array.isArray(data)) {
+ // go into the rabbit hole and dig through that array
+ data.forEach((res, index) => {
+ data[index] = walkRecursive(data[index], fields, callback, index)
+ })
+ } else if (data && typeof data === 'object') {
+ // lets get some keys and stir them
+ Object.keys(data).forEach(k => {
+ data[k] = walkRecursive(data[k], fields, callback, k)
+ })
+ }
+ return data
+}
+
+export default walkRecursive
diff --git a/Human-Connection/backend/src/index.js b/Human-Connection/backend/src/index.js
new file mode 100644
index 000000000..f28e58947
--- /dev/null
+++ b/Human-Connection/backend/src/index.js
@@ -0,0 +1,18 @@
+import createServer from './server'
+import ActivityPub from './activitypub/ActivityPub'
+import CONFIG from './config'
+
+const serverConfig = {
+ port: CONFIG.GRAPHQL_PORT,
+ // cors: {
+ // credentials: true,
+ // origin: [CONFIG.CLIENT_URI] // your frontend url.
+ // }
+}
+
+const server = createServer()
+server.start(serverConfig, options => {
+ /* eslint-disable-next-line no-console */
+ console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`)
+ ActivityPub.init(server)
+})
diff --git a/Human-Connection/backend/src/jest/helpers.js b/Human-Connection/backend/src/jest/helpers.js
new file mode 100644
index 000000000..d07bc9ad1
--- /dev/null
+++ b/Human-Connection/backend/src/jest/helpers.js
@@ -0,0 +1,16 @@
+import { request } from 'graphql-request'
+
+// this is the to-be-tested server host
+// not to be confused with the seeder host
+export const host = 'http://127.0.0.1:4123'
+
+export async function login({ email, password }) {
+ const mutation = `
+ mutation {
+ login(email:"${email}", password:"${password}")
+ }`
+ const response = await request(host, mutation)
+ return {
+ authorization: `Bearer ${response.login}`,
+ }
+}
diff --git a/Human-Connection/backend/src/jwt/decode.js b/Human-Connection/backend/src/jwt/decode.js
new file mode 100644
index 000000000..b98357103
--- /dev/null
+++ b/Human-Connection/backend/src/jwt/decode.js
@@ -0,0 +1,31 @@
+import jwt from 'jsonwebtoken'
+import CONFIG from './../config'
+
+export default async (driver, authorizationHeader) => {
+ if (!authorizationHeader) return null
+ const token = authorizationHeader.replace('Bearer ', '')
+ let id = null
+ try {
+ const decoded = await jwt.verify(token, CONFIG.JWT_SECRET)
+ id = decoded.sub
+ } catch (err) {
+ return null
+ }
+ const session = driver.session()
+ const query = `
+ MATCH (user:User {id: {id} })
+ RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
+ LIMIT 1
+ `
+ const result = await session.run(query, { id })
+ session.close()
+ const [currentUser] = await result.records.map(record => {
+ return record.get('user')
+ })
+ if (!currentUser) return null
+ if (currentUser.disabled) return null
+ return {
+ token,
+ ...currentUser,
+ }
+}
diff --git a/Human-Connection/backend/src/jwt/encode.js b/Human-Connection/backend/src/jwt/encode.js
new file mode 100644
index 000000000..1552804cc
--- /dev/null
+++ b/Human-Connection/backend/src/jwt/encode.js
@@ -0,0 +1,16 @@
+import jwt from 'jsonwebtoken'
+import CONFIG from './../config'
+
+// Generate an Access Token for the given User ID
+export default function encode(user) {
+ const token = jwt.sign(user, CONFIG.JWT_SECRET, {
+ expiresIn: 24 * 60 * 60 * 1000, // one day
+ issuer: CONFIG.GRAPHQL_URI,
+ audience: CONFIG.CLIENT_URI,
+ subject: user.id.toString(),
+ })
+ // jwt.verifySignature(token, CONFIG.JWT_SECRET, (err, data) => {
+ // console.log('token verification:', err, data)
+ // })
+ return token
+}
diff --git a/Human-Connection/backend/src/middleware/activityPubMiddleware.js b/Human-Connection/backend/src/middleware/activityPubMiddleware.js
new file mode 100644
index 000000000..f3ced42f9
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/activityPubMiddleware.js
@@ -0,0 +1,56 @@
+import { generateRsaKeyPair } from '../activitypub/security'
+import { activityPub } from '../activitypub/ActivityPub'
+import as from 'activitystrea.ms'
+
+const debug = require('debug')('backend:schema')
+
+export default {
+ Mutation: {
+ CreatePost: async (resolve, root, args, context, info) => {
+ args.activityId = activityPub.generateStatusId(context.user.slug)
+ args.objectId = activityPub.generateStatusId(context.user.slug)
+
+ const post = await resolve(root, args, context, info)
+
+ const { user: author } = context
+ const actorId = author.actorId
+ debug(`actorId = ${actorId}`)
+ const createActivity = await new Promise((resolve, reject) => {
+ as.create()
+ .id(`${actorId}/status/${args.activityId}`)
+ .actor(`${actorId}`)
+ .object(
+ as
+ .article()
+ .id(`${actorId}/status/${post.id}`)
+ .content(post.content)
+ .to('https://www.w3.org/ns/activitystreams#Public')
+ .publishedNow()
+ .attributedTo(`${actorId}`),
+ )
+ .prettyWrite((err, doc) => {
+ if (err) {
+ reject(err)
+ } else {
+ debug(doc)
+ const parsedDoc = JSON.parse(doc)
+ parsedDoc.send = true
+ resolve(JSON.stringify(parsedDoc))
+ }
+ })
+ })
+ try {
+ await activityPub.sendActivity(createActivity)
+ } catch (e) {
+ debug(`error sending post activity\n${e}`)
+ }
+ return post
+ },
+ CreateUser: async (resolve, root, args, context, info) => {
+ const keys = generateRsaKeyPair()
+ Object.assign(args, keys)
+ args.actorId = `${activityPub.host}/activitypub/users/${args.slug}`
+ return resolve(root, args, context, info)
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/middleware/dateTimeMiddleware.js b/Human-Connection/backend/src/middleware/dateTimeMiddleware.js
new file mode 100644
index 000000000..ac6e0ac4a
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/dateTimeMiddleware.js
@@ -0,0 +1,23 @@
+const setCreatedAt = (resolve, root, args, context, info) => {
+ args.createdAt = new Date().toISOString()
+ return resolve(root, args, context, info)
+}
+const setUpdatedAt = (resolve, root, args, context, info) => {
+ args.updatedAt = new Date().toISOString()
+ return resolve(root, args, context, info)
+}
+
+export default {
+ Mutation: {
+ CreateUser: setCreatedAt,
+ CreatePost: setCreatedAt,
+ CreateComment: setCreatedAt,
+ CreateOrganization: setCreatedAt,
+ CreateNotification: setCreatedAt,
+ UpdateUser: setUpdatedAt,
+ UpdatePost: setUpdatedAt,
+ UpdateComment: setUpdatedAt,
+ UpdateOrganization: setUpdatedAt,
+ UpdateNotification: setUpdatedAt,
+ },
+}
diff --git a/Human-Connection/backend/src/middleware/excerptMiddleware.js b/Human-Connection/backend/src/middleware/excerptMiddleware.js
new file mode 100644
index 000000000..3b3a27c2c
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/excerptMiddleware.js
@@ -0,0 +1,36 @@
+import trunc from 'trunc-html'
+
+export default {
+ Mutation: {
+ CreatePost: async (resolve, root, args, context, info) => {
+ args.contentExcerpt = trunc(args.content, 120).html
+ const result = await resolve(root, args, context, info)
+ return result
+ },
+ UpdatePost: async (resolve, root, args, context, info) => {
+ args.contentExcerpt = trunc(args.content, 120).html
+ const result = await resolve(root, args, context, info)
+ return result
+ },
+ CreateComment: async (resolve, root, args, context, info) => {
+ args.contentExcerpt = trunc(args.content, 180).html
+ const result = await resolve(root, args, context, info)
+ return result
+ },
+ UpdateComment: async (resolve, root, args, context, info) => {
+ args.contentExcerpt = trunc(args.content, 180).html
+ const result = await resolve(root, args, context, info)
+ return result
+ },
+ CreateOrganization: async (resolve, root, args, context, info) => {
+ args.descriptionExcerpt = trunc(args.description, 120).html
+ const result = await resolve(root, args, context, info)
+ return result
+ },
+ UpdateOrganization: async (resolve, root, args, context, info) => {
+ args.descriptionExcerpt = trunc(args.description, 120).html
+ const result = await resolve(root, args, context, info)
+ return result
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/middleware/filterBubble/filterBubble.spec.js b/Human-Connection/backend/src/middleware/filterBubble/filterBubble.spec.js
new file mode 100644
index 000000000..62addeece
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/filterBubble/filterBubble.spec.js
@@ -0,0 +1,77 @@
+import { GraphQLClient } from 'graphql-request'
+import { host, login } from '../../jest/helpers'
+import Factory from '../../seed/factories'
+
+const factory = Factory()
+
+const currentUserParams = {
+ id: 'u1',
+ email: 'you@example.org',
+ name: 'This is you',
+ password: '1234',
+}
+const followedAuthorParams = {
+ id: 'u2',
+ email: 'followed@example.org',
+ name: 'Followed User',
+ password: '1234',
+}
+const randomAuthorParams = {
+ email: 'someone@example.org',
+ name: 'Someone else',
+ password: 'else',
+}
+
+beforeEach(async () => {
+ await Promise.all([
+ factory.create('User', currentUserParams),
+ factory.create('User', followedAuthorParams),
+ factory.create('User', randomAuthorParams),
+ ])
+ const [asYourself, asFollowedUser, asSomeoneElse] = await Promise.all([
+ Factory().authenticateAs(currentUserParams),
+ Factory().authenticateAs(followedAuthorParams),
+ Factory().authenticateAs(randomAuthorParams),
+ ])
+ await asYourself.follow({ id: 'u2', type: 'User' })
+ await asFollowedUser.create('Post', { title: 'This is the post of a followed user' })
+ await asSomeoneElse.create('Post', { title: 'This is some random post' })
+})
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('Filter posts by author is followed by sb.', () => {
+ describe('given an authenticated user', () => {
+ let authenticatedClient
+
+ beforeEach(async () => {
+ const headers = await login(currentUserParams)
+ authenticatedClient = new GraphQLClient(host, { headers })
+ })
+
+ describe('no filter bubble', () => {
+ it('returns all posts', async () => {
+ const query = '{ Post(filter: { }) { title } }'
+ const expected = {
+ Post: [
+ { title: 'This is some random post' },
+ { title: 'This is the post of a followed user' },
+ ],
+ }
+ await expect(authenticatedClient.request(query)).resolves.toEqual(expected)
+ })
+ })
+
+ describe('filtering for posts of followed users only', () => {
+ it('returns only posts authored by followed users', async () => {
+ const query = '{ Post( filter: { author: { followedBy_some: { id: "u1" } } }) { title } }'
+ const expected = {
+ Post: [{ title: 'This is the post of a followed user' }],
+ }
+ await expect(authenticatedClient.request(query)).resolves.toEqual(expected)
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/middleware/includedFieldsMiddleware.js b/Human-Connection/backend/src/middleware/includedFieldsMiddleware.js
new file mode 100644
index 000000000..cd7a74f4e
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/includedFieldsMiddleware.js
@@ -0,0 +1,29 @@
+import cloneDeep from 'lodash/cloneDeep'
+
+const _includeFieldsRecursively = (selectionSet, includedFields) => {
+ if (!selectionSet) return
+ includedFields.forEach(includedField => {
+ selectionSet.selections.unshift({
+ kind: 'Field',
+ name: { kind: 'Name', value: includedField },
+ })
+ })
+ selectionSet.selections.forEach(selection => {
+ _includeFieldsRecursively(selection.selectionSet, includedFields)
+ })
+}
+
+const includeFieldsRecursively = includedFields => {
+ return (resolve, root, args, context, resolveInfo) => {
+ const copy = cloneDeep(resolveInfo)
+ copy.fieldNodes.forEach(fieldNode => {
+ _includeFieldsRecursively(fieldNode.selectionSet, includedFields)
+ })
+ return resolve(root, args, context, copy)
+ }
+}
+
+export default {
+ Query: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted']),
+ Mutation: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted']),
+}
diff --git a/Human-Connection/backend/src/middleware/index.js b/Human-Connection/backend/src/middleware/index.js
new file mode 100644
index 000000000..9b85bd340
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/index.js
@@ -0,0 +1,60 @@
+import CONFIG from './../config'
+import activityPub from './activityPubMiddleware'
+import password from './passwordMiddleware'
+import softDelete from './softDeleteMiddleware'
+import sluggify from './sluggifyMiddleware'
+import excerpt from './excerptMiddleware'
+import dateTime from './dateTimeMiddleware'
+import xss from './xssMiddleware'
+import permissions from './permissionsMiddleware'
+import user from './userMiddleware'
+import includedFields from './includedFieldsMiddleware'
+import orderBy from './orderByMiddleware'
+import validation from './validation'
+import notifications from './notifications'
+
+export default schema => {
+ const middlewares = {
+ permissions: permissions,
+ activityPub: activityPub,
+ password: password,
+ dateTime: dateTime,
+ validation: validation,
+ sluggify: sluggify,
+ excerpt: excerpt,
+ notifications: notifications,
+ xss: xss,
+ softDelete: softDelete,
+ user: user,
+ includedFields: includedFields,
+ orderBy: orderBy,
+ }
+
+ let order = [
+ 'permissions',
+ 'activityPub',
+ 'password',
+ 'dateTime',
+ 'validation',
+ 'sluggify',
+ 'excerpt',
+ 'notifications',
+ 'xss',
+ 'softDelete',
+ 'user',
+ 'includedFields',
+ 'orderBy',
+ ]
+
+ // 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 => {
+ return !disabledMiddlewares.includes(key)
+ })
+ /* eslint-disable-next-line no-console */
+ console.log(`Warning: "${disabledMiddlewares}" middlewares have been disabled.`)
+ }
+
+ return order.map(key => middlewares[key])
+}
diff --git a/Human-Connection/backend/src/middleware/nodes/locations.js b/Human-Connection/backend/src/middleware/nodes/locations.js
new file mode 100644
index 000000000..d7abb90ff
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/nodes/locations.js
@@ -0,0 +1,129 @@
+import request from 'request'
+import { UserInputError } from 'apollo-server'
+import isEmpty from 'lodash/isEmpty'
+import asyncForEach from '../../helpers/asyncForEach'
+import CONFIG from './../../config'
+
+const fetch = url => {
+ return new Promise((resolve, reject) => {
+ request(url, function(error, response, body) {
+ if (error) {
+ reject(error)
+ } else {
+ resolve(JSON.parse(body))
+ }
+ })
+ })
+}
+
+const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl']
+
+const createLocation = async (session, mapboxData) => {
+ const data = {
+ id: mapboxData.id,
+ nameEN: mapboxData.text_en,
+ nameDE: mapboxData.text_de,
+ nameFR: mapboxData.text_fr,
+ nameNL: mapboxData.text_nl,
+ nameIT: mapboxData.text_it,
+ nameES: mapboxData.text_es,
+ namePT: mapboxData.text_pt,
+ namePL: mapboxData.text_pl,
+ type: mapboxData.id.split('.')[0].toLowerCase(),
+ lat: mapboxData.center && mapboxData.center.length ? mapboxData.center[0] : null,
+ lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null,
+ }
+
+ let query =
+ 'MERGE (l:Location {id: $id}) ' +
+ 'SET l.name = $nameEN, ' +
+ 'l.nameEN = $nameEN, ' +
+ 'l.nameDE = $nameDE, ' +
+ 'l.nameFR = $nameFR, ' +
+ 'l.nameNL = $nameNL, ' +
+ 'l.nameIT = $nameIT, ' +
+ 'l.nameES = $nameES, ' +
+ 'l.namePT = $namePT, ' +
+ 'l.namePL = $namePL, ' +
+ 'l.type = $type'
+
+ if (data.lat && data.lng) {
+ query += ', l.lat = $lat, l.lng = $lng'
+ }
+ query += ' RETURN l.id'
+
+ await session.run(query, data)
+}
+
+const createOrUpdateLocations = async (userId, locationName, driver) => {
+ if (isEmpty(locationName)) {
+ return
+ }
+ const res = await fetch(
+ `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
+ locationName,
+ )}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${locales.join(
+ ',',
+ )}`,
+ )
+
+ if (!res || !res.features || !res.features[0]) {
+ throw new UserInputError('locationName is invalid')
+ }
+
+ let data
+
+ res.features.forEach(item => {
+ if (item.matching_place_name === locationName) {
+ data = item
+ }
+ })
+ if (!data) {
+ data = res.features[0]
+ }
+
+ if (!data || !data.place_type || !data.place_type.length) {
+ throw new UserInputError('locationName is invalid')
+ }
+
+ const session = driver.session()
+ if (data.place_type.length > 1) {
+ data.id = 'region.' + data.id.split('.')[1]
+ }
+ await createLocation(session, data)
+
+ let parent = data
+
+ if (data.context) {
+ await asyncForEach(data.context, async ctx => {
+ await createLocation(session, ctx)
+
+ await session.run(
+ 'MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) ' +
+ 'MERGE (child)<-[:IS_IN]-(parent) ' +
+ 'RETURN child.id, parent.id',
+ {
+ parentId: parent.id,
+ childId: ctx.id,
+ },
+ )
+
+ parent = ctx
+ })
+ }
+ // delete all current locations from user
+ await session.run('MATCH (u:User {id: $userId})-[r:IS_IN]->(l:Location) DETACH DELETE r', {
+ userId: userId,
+ })
+ // connect user with location
+ await session.run(
+ 'MATCH (u:User {id: $userId}), (l:Location {id: $locationId}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id',
+ {
+ userId: userId,
+ locationId: data.id,
+ },
+ )
+ session.close()
+}
+
+export default createOrUpdateLocations
diff --git a/Human-Connection/backend/src/middleware/notifications/extractIds/index.js b/Human-Connection/backend/src/middleware/notifications/extractIds/index.js
new file mode 100644
index 000000000..c2fcf169c
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/notifications/extractIds/index.js
@@ -0,0 +1,20 @@
+import cheerio from 'cheerio'
+const ID_REGEX = /\/profile\/([\w\-.!~*'"(),]+)/g
+
+export default function(content) {
+ if (!content) return []
+ const $ = cheerio.load(content)
+ const urls = $('.mention')
+ .map((_, el) => {
+ return $(el).attr('href')
+ })
+ .get()
+ const ids = []
+ urls.forEach(url => {
+ let match
+ while ((match = ID_REGEX.exec(url)) != null) {
+ ids.push(match[1])
+ }
+ })
+ return ids
+}
diff --git a/Human-Connection/backend/src/middleware/notifications/extractIds/spec.js b/Human-Connection/backend/src/middleware/notifications/extractIds/spec.js
new file mode 100644
index 000000000..341c39cec
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/notifications/extractIds/spec.js
@@ -0,0 +1,59 @@
+import extractIds from '.'
+
+describe('extractIds', () => {
+ describe('content undefined', () => {
+ it('returns empty array', () => {
+ expect(extractIds()).toEqual([])
+ })
+ })
+
+ describe('searches through links', () => {
+ it('ignores links without .mention class', () => {
+ const content =
+ 'Something inspirational about @bob-der-baumeister and @jenny-rostock.
'
+ expect(extractIds(content)).toEqual([])
+ })
+
+ describe('given a link with .mention class', () => {
+ it('extracts ids', () => {
+ const content =
+ 'Something inspirational about @bob-der-baumeister and @jenny-rostock.
'
+ expect(extractIds(content)).toEqual(['u2', 'u3'])
+ })
+
+ describe('handles links', () => {
+ it('with slug and id', () => {
+ const content =
+ 'Something inspirational about @bob-der-baumeister and @jenny-rostock.
'
+ expect(extractIds(content)).toEqual(['u2', 'u3'])
+ })
+
+ it('with domains', () => {
+ const content =
+ 'Something inspirational about @bob-der-baumeister and @jenny-rostock.
'
+ expect(extractIds(content)).toEqual(['u2', 'u3'])
+ })
+
+ it('special characters', () => {
+ const content =
+ 'Something inspirational about @bob-der-baumeister and @jenny-rostock.
'
+ expect(extractIds(content)).toEqual(['u!*(),2', 'u.~-3'])
+ })
+ })
+
+ describe('does not crash if', () => {
+ it('`href` contains no user id', () => {
+ const content =
+ 'Something inspirational about @bob-der-baumeister and @jenny-rostock.
'
+ expect(extractIds(content)).toEqual([])
+ })
+
+ it('`href` is empty or invalid', () => {
+ const content =
+ 'Something inspirational about @bob-der-baumeister and @jenny-rostock.
'
+ expect(extractIds(content)).toEqual([])
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/middleware/notifications/index.js b/Human-Connection/backend/src/middleware/notifications/index.js
new file mode 100644
index 000000000..ca460a512
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/notifications/index.js
@@ -0,0 +1,30 @@
+import extractIds from './extractIds'
+
+const notify = async (resolve, root, args, context, resolveInfo) => {
+ // extract user ids before xss-middleware removes link classes
+ const ids = extractIds(args.content)
+
+ const post = await resolve(root, args, context, resolveInfo)
+
+ const session = context.driver.session()
+ const { id: postId } = post
+ const createdAt = new Date().toISOString()
+ const cypher = `
+ match(u:User) where u.id in $ids
+ match(p:Post) where p.id = $postId
+ create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt})
+ merge (n)-[:NOTIFIED]->(u)
+ merge (p)-[:NOTIFIED]->(n)
+ `
+ await session.run(cypher, { ids, createdAt, postId })
+ session.close()
+
+ return post
+}
+
+export default {
+ Mutation: {
+ CreatePost: notify,
+ UpdatePost: notify,
+ },
+}
diff --git a/Human-Connection/backend/src/middleware/notifications/spec.js b/Human-Connection/backend/src/middleware/notifications/spec.js
new file mode 100644
index 000000000..d214a5571
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/notifications/spec.js
@@ -0,0 +1,130 @@
+import { GraphQLClient } from 'graphql-request'
+import { host, login } from '../../jest/helpers'
+import Factory from '../../seed/factories'
+
+const factory = Factory()
+let client
+
+beforeEach(async () => {
+ await factory.create('User', {
+ id: 'you',
+ name: 'Al Capone',
+ slug: 'al-capone',
+ email: 'test@example.org',
+ password: '1234',
+ })
+})
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('currentUser { notifications }', () => {
+ const query = `query($read: Boolean) {
+ currentUser {
+ notifications(read: $read, orderBy: createdAt_desc) {
+ read
+ post {
+ content
+ }
+ }
+ }
+ }`
+
+ describe('authenticated', () => {
+ let headers
+ beforeEach(async () => {
+ headers = await login({ email: 'test@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ describe('given another user', () => {
+ let authorClient
+ let authorParams
+ let authorHeaders
+
+ beforeEach(async () => {
+ authorParams = {
+ email: 'author@example.org',
+ password: '1234',
+ id: 'author',
+ }
+ await factory.create('User', authorParams)
+ authorHeaders = await login(authorParams)
+ })
+
+ describe('who mentions me in a post', () => {
+ let post
+ const title = 'Mentioning Al Capone'
+ const content =
+ 'Hey @al-capone how do you do?'
+
+ beforeEach(async () => {
+ const createPostMutation = `
+ mutation($title: String!, $content: String!) {
+ CreatePost(title: $title, content: $content) {
+ id
+ title
+ content
+ }
+ }
+ `
+ authorClient = new GraphQLClient(host, { headers: authorHeaders })
+ const { CreatePost } = await authorClient.request(createPostMutation, { title, content })
+ post = CreatePost
+ })
+
+ it('sends you a notification', async () => {
+ const expectedContent =
+ 'Hey @al-capone how do you do?'
+ const expected = {
+ currentUser: {
+ notifications: [{ read: false, post: { content: expectedContent } }],
+ },
+ }
+ await expect(client.request(query, { read: false })).resolves.toEqual(expected)
+ })
+
+ describe('who mentions me again', () => {
+ beforeEach(async () => {
+ const updatedContent = `${post.content} One more mention to @al-capone`
+ const updatedTitle = 'this post has been updated'
+ // The response `post.content` contains a link but the XSSmiddleware
+ // should have the `mention` CSS class removed. I discovered this
+ // during development and thought: A feature not a bug! This way we
+ // can encode a re-mentioning of users when you edit your post or
+ // comment.
+ const updatePostMutation = `
+ mutation($id: ID!, $title: String!, $content: String!) {
+ UpdatePost(id: $id, title: $title, content: $content) {
+ title
+ content
+ }
+ }
+ `
+ authorClient = new GraphQLClient(host, { headers: authorHeaders })
+ await authorClient.request(updatePostMutation, {
+ id: post.id,
+ content: updatedContent,
+ title: updatedTitle,
+ })
+ })
+
+ it('creates exactly one more notification', async () => {
+ const expectedContent =
+ 'Hey @al-capone how do you do? One more mention to @al-capone'
+ const expected = {
+ currentUser: {
+ notifications: [
+ { read: false, post: { content: expectedContent } },
+ { read: false, post: { content: expectedContent } },
+ ],
+ },
+ }
+ await expect(client.request(query, { read: false })).resolves.toEqual(expected)
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/middleware/orderByMiddleware.js b/Human-Connection/backend/src/middleware/orderByMiddleware.js
new file mode 100644
index 000000000..64eac8b74
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/orderByMiddleware.js
@@ -0,0 +1,19 @@
+import cloneDeep from 'lodash/cloneDeep'
+
+const defaultOrderBy = (resolve, root, args, context, resolveInfo) => {
+ const copy = cloneDeep(resolveInfo)
+ const newestFirst = {
+ kind: 'Argument',
+ name: { kind: 'Name', value: 'orderBy' },
+ value: { kind: 'EnumValue', value: 'createdAt_desc' },
+ }
+ const [fieldNode] = copy.fieldNodes
+ if (fieldNode) fieldNode.arguments.push(newestFirst)
+ return resolve(root, args, context, copy)
+}
+
+export default {
+ Query: {
+ Post: defaultOrderBy,
+ },
+}
diff --git a/Human-Connection/backend/src/middleware/orderByMiddleware.spec.js b/Human-Connection/backend/src/middleware/orderByMiddleware.spec.js
new file mode 100644
index 000000000..450220cd6
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/orderByMiddleware.spec.js
@@ -0,0 +1,62 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../seed/factories'
+import { host } from '../jest/helpers'
+
+let client
+let headers
+let query
+const factory = Factory()
+
+beforeEach(async () => {
+ const userParams = { name: 'Author', email: 'author@example.org', password: '1234' }
+ await factory.create('User', userParams)
+ await factory.authenticateAs(userParams)
+ await factory.create('Post', { title: 'first' })
+ await factory.create('Post', { title: 'second' })
+ await factory.create('Post', { title: 'third' })
+ await factory.create('Post', { title: 'last' })
+ headers = {}
+ client = new GraphQLClient(host, { headers })
+})
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('Query', () => {
+ describe('Post', () => {
+ beforeEach(() => {
+ query = '{ Post { title } }'
+ })
+
+ describe('orderBy', () => {
+ it('createdAt descending is default', async () => {
+ const posts = [
+ { title: 'last' },
+ { title: 'third' },
+ { title: 'second' },
+ { title: 'first' },
+ ]
+ const expected = { Post: posts }
+ await expect(client.request(query)).resolves.toEqual(expected)
+ })
+
+ describe('(orderBy: createdAt_asc)', () => {
+ beforeEach(() => {
+ query = '{ Post(orderBy: createdAt_asc) { title } }'
+ })
+
+ it('orders by createdAt ascending', async () => {
+ const posts = [
+ { title: 'first' },
+ { title: 'second' },
+ { title: 'third' },
+ { title: 'last' },
+ ]
+ const expected = { Post: posts }
+ await expect(client.request(query)).resolves.toEqual(expected)
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/middleware/passwordMiddleware.js b/Human-Connection/backend/src/middleware/passwordMiddleware.js
new file mode 100644
index 000000000..1078e5529
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/passwordMiddleware.js
@@ -0,0 +1,21 @@
+import bcrypt from 'bcryptjs'
+import walkRecursive from '../helpers/walkRecursive'
+
+export default {
+ Mutation: {
+ CreateUser: async (resolve, root, args, context, info) => {
+ args.password = await bcrypt.hashSync(args.password, 10)
+ const result = await resolve(root, args, context, info)
+ result.password = '*****'
+ return result
+ },
+ },
+ Query: async (resolve, root, args, context, info) => {
+ let result = await resolve(root, args, context, info)
+ result = walkRecursive(result, ['password', 'privateKey'], () => {
+ // replace password with asterisk
+ return '*****'
+ })
+ return result
+ },
+}
diff --git a/Human-Connection/backend/src/middleware/permissionsMiddleware.js b/Human-Connection/backend/src/middleware/permissionsMiddleware.js
new file mode 100644
index 000000000..af4a46d81
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/permissionsMiddleware.js
@@ -0,0 +1,166 @@
+import { rule, shield, deny, allow, or } from 'graphql-shield'
+
+/*
+ * TODO: implement
+ * See: https://github.com/Human-Connection/Nitro-Backend/pull/40#pullrequestreview-180898363
+ */
+const isAuthenticated = rule({
+ cache: 'contextual',
+})(async (_parent, _args, ctx, _info) => {
+ return ctx.user !== null
+})
+
+const isModerator = rule()(async (parent, args, { user }, info) => {
+ return user && (user.role === 'moderator' || user.role === 'admin')
+})
+
+const isAdmin = rule()(async (parent, args, { user }, info) => {
+ return user && user.role === 'admin'
+})
+
+const onlyYourself = rule({
+ cache: 'no_cache',
+})(async (parent, args, context, info) => {
+ return context.user.id === args.id
+})
+
+const isMyOwn = rule({
+ cache: 'no_cache',
+})(async (parent, args, context, info) => {
+ return context.user.id === parent.id
+})
+
+const belongsToMe = rule({
+ cache: 'no_cache',
+})(async (_, args, context) => {
+ const {
+ driver,
+ user: { id: userId },
+ } = context
+ const { id: notificationId } = args
+ const session = driver.session()
+ const result = await session.run(
+ `
+ MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId})
+ RETURN n
+ `,
+ {
+ userId,
+ notificationId,
+ },
+ )
+ const [notification] = result.records.map(record => {
+ return record.get('n')
+ })
+ session.close()
+ return Boolean(notification)
+})
+
+/* TODO: decide if we want to remove this check: the check
+ * `onlyEnabledContent` throws authorization errors only if you have
+ * arguments for `disabled` or `deleted` assuming these are filter
+ * parameters. Soft-delete middleware obfuscates data on its way out
+ * anyways. Furthermore, `neo4j-graphql-js` offers many ways to filter for
+ * data so I believe, this is not a good check anyways.
+ */
+const onlyEnabledContent = rule({
+ cache: 'strict',
+})(async (parent, args, ctx, info) => {
+ const { disabled, deleted } = args
+ return !(disabled || deleted)
+})
+
+const isAuthor = rule({
+ cache: 'no_cache',
+})(async (parent, args, { user, driver }) => {
+ if (!user) return false
+ const session = driver.session()
+ const { id: resourceId } = args
+ const result = await session.run(
+ `
+ MATCH (resource {id: $resourceId})<-[:WROTE]-(author)
+ RETURN author
+ `,
+ {
+ resourceId,
+ },
+ )
+ const [author] = result.records.map(record => {
+ return record.get('author')
+ })
+ const {
+ properties: { id: authorId },
+ } = author
+ session.close()
+ return authorId === user.id
+})
+
+const isDeletingOwnAccount = rule({
+ cache: 'no_cache',
+})(async (parent, args, context, info) => {
+ return context.user.id === args.id
+})
+
+// Permissions
+const permissions = shield(
+ {
+ Query: {
+ '*': deny,
+ findPosts: allow,
+ Category: allow,
+ Tag: isAdmin,
+ Report: isModerator,
+ Notification: isAdmin,
+ statistics: allow,
+ currentUser: allow,
+ Post: or(onlyEnabledContent, isModerator),
+ Comment: allow,
+ User: allow,
+ isLoggedIn: allow,
+ },
+ Mutation: {
+ '*': deny,
+ login: allow,
+ UpdateNotification: belongsToMe,
+ CreateUser: isAdmin,
+ UpdateUser: onlyYourself,
+ CreatePost: isAuthenticated,
+ UpdatePost: isAuthor,
+ DeletePost: isAuthor,
+ report: isAuthenticated,
+ CreateBadge: isAdmin,
+ UpdateBadge: isAdmin,
+ DeleteBadge: isAdmin,
+ AddUserBadges: isAdmin,
+ CreateSocialMedia: isAuthenticated,
+ DeleteSocialMedia: isAuthenticated,
+ // AddBadgeRewarded: isAdmin,
+ // RemoveBadgeRewarded: isAdmin,
+ reward: isAdmin,
+ unreward: isAdmin,
+ // addFruitToBasket: isAuthenticated
+ follow: isAuthenticated,
+ unfollow: isAuthenticated,
+ shout: isAuthenticated,
+ unshout: isAuthenticated,
+ changePassword: isAuthenticated,
+ enable: isModerator,
+ disable: isModerator,
+ CreateComment: isAuthenticated,
+ DeleteComment: isAuthor,
+ DeleteUser: isDeletingOwnAccount,
+ requestPasswordReset: allow,
+ resetPassword: allow,
+ },
+ User: {
+ email: isMyOwn,
+ password: isMyOwn,
+ privateKey: isMyOwn,
+ },
+ },
+ {
+ fallbackRule: allow,
+ },
+)
+
+export default permissions
diff --git a/Human-Connection/backend/src/middleware/permissionsMiddleware.spec.js b/Human-Connection/backend/src/middleware/permissionsMiddleware.spec.js
new file mode 100644
index 000000000..6cf9dc302
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/permissionsMiddleware.spec.js
@@ -0,0 +1,91 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../seed/factories'
+import { host, login } from '../jest/helpers'
+
+const factory = Factory()
+
+describe('authorization', () => {
+ describe('given two existing users', () => {
+ beforeEach(async () => {
+ await factory.create('User', {
+ email: 'owner@example.org',
+ name: 'Owner',
+ password: 'iamtheowner',
+ })
+ await factory.create('User', {
+ email: 'someone@example.org',
+ name: 'Someone else',
+ password: 'else',
+ })
+ })
+
+ afterEach(async () => {
+ await factory.cleanDatabase()
+ })
+
+ describe('access email address', () => {
+ let headers = {}
+ let loginCredentials = null
+ const action = async () => {
+ if (loginCredentials) {
+ headers = await login(loginCredentials)
+ }
+ const graphQLClient = new GraphQLClient(host, { headers })
+ return graphQLClient.request('{User(name: "Owner") { email } }')
+ }
+
+ describe('not logged in', () => {
+ it('rejects', async () => {
+ await expect(action()).rejects.toThrow('Not Authorised!')
+ })
+
+ it("does not expose the owner's email address", async () => {
+ let response = {}
+ try {
+ await action()
+ } catch (error) {
+ response = error.response.data
+ } finally {
+ expect(response).toEqual({ User: [null] })
+ }
+ })
+ })
+
+ describe('as owner', () => {
+ beforeEach(() => {
+ loginCredentials = {
+ email: 'owner@example.org',
+ password: 'iamtheowner',
+ }
+ })
+
+ it("exposes the owner's email address", async () => {
+ await expect(action()).resolves.toEqual({ User: [{ email: 'owner@example.org' }] })
+ })
+ })
+
+ describe('authenticated as another user', () => {
+ beforeEach(async () => {
+ loginCredentials = {
+ email: 'someone@example.org',
+ password: 'else',
+ }
+ })
+
+ it('rejects', async () => {
+ await expect(action()).rejects.toThrow('Not Authorised!')
+ })
+
+ it("does not expose the owner's email address", async () => {
+ let response
+ try {
+ await action()
+ } catch (error) {
+ response = error.response.data
+ }
+ expect(response).toEqual({ User: [null] })
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/middleware/sluggifyMiddleware.js b/Human-Connection/backend/src/middleware/sluggifyMiddleware.js
new file mode 100644
index 000000000..226bef8e5
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/sluggifyMiddleware.js
@@ -0,0 +1,37 @@
+import uniqueSlug from './slugify/uniqueSlug'
+
+const isUniqueFor = (context, type) => {
+ return async slug => {
+ const session = context.driver.session()
+ const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, {
+ slug,
+ })
+ session.close()
+ return response.records.length === 0
+ }
+}
+
+export default {
+ Mutation: {
+ CreatePost: async (resolve, root, args, context, info) => {
+ args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
+ return resolve(root, args, context, info)
+ },
+ UpdatePost: async (resolve, root, args, context, info) => {
+ args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
+ return resolve(root, args, context, info)
+ },
+ CreateUser: async (resolve, root, args, context, info) => {
+ args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
+ return resolve(root, args, context, info)
+ },
+ CreateOrganization: async (resolve, root, args, context, info) => {
+ args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Organization')))
+ return resolve(root, args, context, info)
+ },
+ CreateCategory: async (resolve, root, args, context, info) => {
+ args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Category')))
+ return resolve(root, args, context, info)
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/middleware/slugify/uniqueSlug.js b/Human-Connection/backend/src/middleware/slugify/uniqueSlug.js
new file mode 100644
index 000000000..69aef2d1b
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/slugify/uniqueSlug.js
@@ -0,0 +1,15 @@
+import slugify from 'slug'
+export default async function uniqueSlug(string, isUnique) {
+ let slug = slugify(string || 'anonymous', {
+ lower: true,
+ })
+ if (await isUnique(slug)) return slug
+
+ let count = 0
+ let uniqueSlug
+ do {
+ count += 1
+ uniqueSlug = `${slug}-${count}`
+ } while (!(await isUnique(uniqueSlug)))
+ return uniqueSlug
+}
diff --git a/Human-Connection/backend/src/middleware/slugify/uniqueSlug.spec.js b/Human-Connection/backend/src/middleware/slugify/uniqueSlug.spec.js
new file mode 100644
index 000000000..e34af86a1
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/slugify/uniqueSlug.spec.js
@@ -0,0 +1,24 @@
+import uniqueSlug from './uniqueSlug'
+
+describe('uniqueSlug', () => {
+ it('slugifies given string', () => {
+ const string = 'Hello World'
+ const isUnique = jest.fn().mockResolvedValue(true)
+ expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world')
+ })
+
+ it('increments slugified string until unique', () => {
+ const string = 'Hello World'
+ const isUnique = jest
+ .fn()
+ .mockResolvedValueOnce(false)
+ .mockResolvedValueOnce(true)
+ expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world-1')
+ })
+
+ it('slugify null string', () => {
+ const string = null
+ const isUnique = jest.fn().mockResolvedValue(true)
+ expect(uniqueSlug(string, isUnique)).resolves.toEqual('anonymous')
+ })
+})
diff --git a/Human-Connection/backend/src/middleware/slugifyMiddleware.spec.js b/Human-Connection/backend/src/middleware/slugifyMiddleware.spec.js
new file mode 100644
index 000000000..4e060dc90
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/slugifyMiddleware.spec.js
@@ -0,0 +1,111 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../seed/factories'
+import { host, login } from '../jest/helpers'
+
+let authenticatedClient
+let headers
+const factory = Factory()
+
+beforeEach(async () => {
+ const adminParams = { role: 'admin', email: 'admin@example.org', password: '1234' }
+ await factory.create('User', adminParams)
+ await factory.create('User', {
+ email: 'someone@example.org',
+ password: '1234',
+ })
+ // we need to be an admin, otherwise we're not authorized to create a user
+ headers = await login(adminParams)
+ authenticatedClient = new GraphQLClient(host, { headers })
+})
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('slugify', () => {
+ describe('CreatePost', () => {
+ it('generates a slug based on title', async () => {
+ const response = await authenticatedClient.request(`mutation {
+ CreatePost(
+ title: "I am a brand new post",
+ content: "Some content"
+ ) { slug }
+ }`)
+ expect(response).toEqual({
+ CreatePost: { slug: 'i-am-a-brand-new-post' },
+ })
+ })
+
+ describe('if slug exists', () => {
+ beforeEach(async () => {
+ const asSomeoneElse = await Factory().authenticateAs({
+ email: 'someone@example.org',
+ password: '1234',
+ })
+ await asSomeoneElse.create('Post', {
+ title: 'Pre-existing post',
+ slug: 'pre-existing-post',
+ })
+ })
+
+ it('chooses another slug', async () => {
+ const response = await authenticatedClient.request(`mutation {
+ CreatePost(
+ title: "Pre-existing post",
+ content: "Some content"
+ ) { slug }
+ }`)
+ expect(response).toEqual({
+ CreatePost: { slug: 'pre-existing-post-1' },
+ })
+ })
+
+ describe('but if the client specifies a slug', () => {
+ it('rejects CreatePost', async () => {
+ await expect(
+ authenticatedClient.request(`mutation {
+ CreatePost(
+ title: "Pre-existing post",
+ content: "Some content",
+ slug: "pre-existing-post"
+ ) { slug }
+ }`),
+ ).rejects.toThrow('already exists')
+ })
+ })
+ })
+ })
+
+ describe('CreateUser', () => {
+ const action = async (mutation, params) => {
+ return authenticatedClient.request(`mutation {
+ ${mutation}(password: "yo", email: "123@123.de", ${params}) { slug }
+ }`)
+ }
+ it('generates a slug based on name', async () => {
+ await expect(action('CreateUser', 'name: "I am a user"')).resolves.toEqual({
+ CreateUser: { slug: 'i-am-a-user' },
+ })
+ })
+
+ describe('if slug exists', () => {
+ beforeEach(async () => {
+ await action('CreateUser', 'name: "Pre-existing user", slug: "pre-existing-user"')
+ })
+
+ it('chooses another slug', async () => {
+ await expect(action('CreateUser', 'name: "pre-existing-user"')).resolves.toEqual({
+ CreateUser: { slug: 'pre-existing-user-1' },
+ })
+ })
+
+ describe('but if the client specifies a slug', () => {
+ it('rejects CreateUser', async () => {
+ await expect(
+ action('CreateUser', 'name: "Pre-existing user", slug: "pre-existing-user"'),
+ ).rejects.toThrow('already exists')
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/middleware/softDeleteMiddleware.js b/Human-Connection/backend/src/middleware/softDeleteMiddleware.js
new file mode 100644
index 000000000..cc5aa06c5
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/softDeleteMiddleware.js
@@ -0,0 +1,46 @@
+const isModerator = ({ user }) => {
+ return user && (user.role === 'moderator' || user.role === 'admin')
+}
+
+const setDefaultFilters = (resolve, root, args, context, info) => {
+ if (typeof args.deleted !== 'boolean') {
+ args.deleted = false
+ }
+
+ if (!isModerator(context)) {
+ args.disabled = false
+ }
+ return resolve(root, args, context, info)
+}
+
+const obfuscateDisabled = async (resolve, root, args, context, info) => {
+ if (!isModerator(context) && root.disabled) {
+ root.content = 'UNAVAILABLE'
+ root.contentExcerpt = 'UNAVAILABLE'
+ root.title = 'UNAVAILABLE'
+ root.image = 'UNAVAILABLE'
+ root.avatar = 'UNAVAILABLE'
+ root.about = 'UNAVAILABLE'
+ root.name = 'UNAVAILABLE'
+ }
+ return resolve(root, args, context, info)
+}
+
+export default {
+ Query: {
+ Post: setDefaultFilters,
+ Comment: setDefaultFilters,
+ User: setDefaultFilters,
+ },
+ Mutation: async (resolve, root, args, context, info) => {
+ args.disabled = false
+ // TODO: remove as soon as our factories don't need this anymore
+ if (typeof args.deleted !== 'boolean') {
+ args.deleted = false
+ }
+ return resolve(root, args, context, info)
+ },
+ Post: obfuscateDisabled,
+ User: obfuscateDisabled,
+ Comment: obfuscateDisabled,
+}
diff --git a/Human-Connection/backend/src/middleware/softDeleteMiddleware.spec.js b/Human-Connection/backend/src/middleware/softDeleteMiddleware.spec.js
new file mode 100644
index 000000000..388f44a3c
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/softDeleteMiddleware.spec.js
@@ -0,0 +1,296 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../seed/factories'
+import { host, login } from '../jest/helpers'
+
+const factory = Factory()
+let client
+let query
+let action
+
+beforeAll(async () => {
+ // For performance reasons we do this only once
+ await Promise.all([
+ factory.create('User', { id: 'u1', role: 'user', email: 'user@example.org', password: '1234' }),
+ factory.create('User', {
+ id: 'm1',
+ role: 'moderator',
+ email: 'moderator@example.org',
+ password: '1234',
+ }),
+ factory.create('User', {
+ id: 'u2',
+ role: 'user',
+ name: 'Offensive Name',
+ avatar: '/some/offensive/avatar.jpg',
+ about: 'This self description is very offensive',
+ email: 'troll@example.org',
+ password: '1234',
+ }),
+ ])
+
+ await factory.authenticateAs({ email: 'user@example.org', password: '1234' })
+ await Promise.all([
+ factory.follow({ id: 'u2', type: 'User' }),
+ factory.create('Post', { id: 'p1', title: 'Deleted post', deleted: true }),
+ factory.create('Post', { id: 'p3', title: 'Publicly visible post', deleted: false }),
+ ])
+
+ await Promise.all([
+ factory.create('Comment', {
+ id: 'c2',
+ postId: 'p3',
+ content: 'Enabled comment on public post',
+ }),
+ ])
+
+ await Promise.all([factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' })])
+
+ const asTroll = Factory()
+ await asTroll.authenticateAs({ email: 'troll@example.org', password: '1234' })
+ await asTroll.create('Post', {
+ id: 'p2',
+ title: 'Disabled post',
+ content: 'This is an offensive post content',
+ image: '/some/offensive/image.jpg',
+ deleted: false,
+ })
+ await asTroll.create('Comment', { id: 'c1', postId: 'p3', content: 'Disabled comment' })
+ await Promise.all([asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' })])
+
+ const asModerator = Factory()
+ await asModerator.authenticateAs({ email: 'moderator@example.org', password: '1234' })
+ await asModerator.mutate('mutation { disable( id: "p2") }')
+ await asModerator.mutate('mutation { disable( id: "c1") }')
+ await asModerator.mutate('mutation { disable( id: "u2") }')
+})
+
+afterAll(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('softDeleteMiddleware', () => {
+ describe('read disabled content', () => {
+ let user
+ let post
+ let comment
+ const beforeComment = async () => {
+ query = '{ User(id: "u1") { following { comments { content contentExcerpt } } } }'
+ const response = await action()
+ comment = response.User[0].following[0].comments[0]
+ }
+ const beforeUser = async () => {
+ query = '{ User(id: "u1") { following { name about avatar } } }'
+ const response = await action()
+ user = response.User[0].following[0]
+ }
+ const beforePost = async () => {
+ query =
+ '{ User(id: "u1") { following { contributions { title image content contentExcerpt } } } }'
+ const response = await action()
+ post = response.User[0].following[0].contributions[0]
+ }
+
+ action = () => {
+ return client.request(query)
+ }
+
+ describe('as moderator', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'moderator@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ describe('User', () => {
+ beforeEach(beforeUser)
+
+ it('displays name', () => expect(user.name).toEqual('Offensive Name'))
+ it('displays about', () =>
+ expect(user.about).toEqual('This self description is very offensive'))
+ it('displays avatar', () => expect(user.avatar).toEqual('/some/offensive/avatar.jpg'))
+ })
+
+ describe('Post', () => {
+ beforeEach(beforePost)
+
+ it('displays title', () => expect(post.title).toEqual('Disabled post'))
+ it('displays content', () =>
+ expect(post.content).toEqual('This is an offensive post content'))
+ it('displays contentExcerpt', () =>
+ expect(post.contentExcerpt).toEqual('This is an offensive post content'))
+ it('displays image', () => expect(post.image).toEqual('/some/offensive/image.jpg'))
+ })
+
+ describe('Comment', () => {
+ beforeEach(beforeComment)
+
+ it('displays content', () => expect(comment.content).toEqual('Disabled comment'))
+ it('displays contentExcerpt', () =>
+ expect(comment.contentExcerpt).toEqual('Disabled comment'))
+ })
+ })
+
+ describe('as user', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'user@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ describe('User', () => {
+ beforeEach(beforeUser)
+
+ it('displays name', () => expect(user.name).toEqual('UNAVAILABLE'))
+ it('obfuscates about', () => expect(user.about).toEqual('UNAVAILABLE'))
+ it('obfuscates avatar', () => expect(user.avatar).toEqual('UNAVAILABLE'))
+ })
+
+ describe('Post', () => {
+ beforeEach(beforePost)
+
+ it('obfuscates title', () => expect(post.title).toEqual('UNAVAILABLE'))
+ it('obfuscates content', () => expect(post.content).toEqual('UNAVAILABLE'))
+ it('obfuscates contentExcerpt', () => expect(post.contentExcerpt).toEqual('UNAVAILABLE'))
+ it('obfuscates image', () => expect(post.image).toEqual('UNAVAILABLE'))
+ })
+
+ describe('Comment', () => {
+ beforeEach(beforeComment)
+
+ it('obfuscates content', () => expect(comment.content).toEqual('UNAVAILABLE'))
+ it('obfuscates contentExcerpt', () => expect(comment.contentExcerpt).toEqual('UNAVAILABLE'))
+ })
+ })
+ })
+
+ describe('Query', () => {
+ describe('Post', () => {
+ beforeEach(async () => {
+ query = '{ Post { title } }'
+ })
+
+ describe('as user', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'user@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('hides deleted or disabled posts', async () => {
+ const expected = { Post: [{ title: 'Publicly visible post' }] }
+ await expect(action()).resolves.toEqual(expected)
+ })
+ })
+
+ describe('as moderator', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'moderator@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('shows disabled but hides deleted posts', async () => {
+ const expected = [{ title: 'Disabled post' }, { title: 'Publicly visible post' }]
+ const { Post } = await action()
+ await expect(Post).toEqual(expect.arrayContaining(expected))
+ })
+ })
+
+ describe('.comments', () => {
+ beforeEach(async () => {
+ query = '{ Post(id: "p3") { title comments { content } } }'
+ })
+
+ describe('as user', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'user@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('conceals disabled comments', async () => {
+ const expected = [
+ { content: 'Enabled comment on public post' },
+ { content: 'UNAVAILABLE' },
+ ]
+ const {
+ Post: [{ comments }],
+ } = await action()
+ await expect(comments).toEqual(expect.arrayContaining(expected))
+ })
+ })
+
+ describe('as moderator', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'moderator@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('shows disabled comments', async () => {
+ const expected = [
+ { content: 'Enabled comment on public post' },
+ { content: 'Disabled comment' },
+ ]
+ const {
+ Post: [{ comments }],
+ } = await action()
+ await expect(comments).toEqual(expect.arrayContaining(expected))
+ })
+ })
+ })
+
+ describe('filter (deleted: true)', () => {
+ beforeEach(() => {
+ query = '{ Post(deleted: true) { title } }'
+ })
+
+ describe('as user', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'user@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('throws authorisation error', async () => {
+ await expect(action()).rejects.toThrow('Not Authorised!')
+ })
+ })
+
+ describe('as moderator', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'moderator@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('shows deleted posts', async () => {
+ const expected = { Post: [{ title: 'Deleted post' }] }
+ await expect(action()).resolves.toEqual(expected)
+ })
+ })
+ })
+
+ describe('filter (disabled: true)', () => {
+ beforeEach(() => {
+ query = '{ Post(disabled: true) { title } }'
+ })
+
+ describe('as user', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'user@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('throws authorisation error', async () => {
+ await expect(action()).rejects.toThrow('Not Authorised!')
+ })
+ })
+
+ describe('as moderator', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'moderator@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('shows disabled posts', async () => {
+ const expected = { Post: [{ title: 'Disabled post' }] }
+ await expect(action()).resolves.toEqual(expected)
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/middleware/userMiddleware.js b/Human-Connection/backend/src/middleware/userMiddleware.js
new file mode 100644
index 000000000..29e512ebd
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/userMiddleware.js
@@ -0,0 +1,16 @@
+import createOrUpdateLocations from './nodes/locations'
+
+export default {
+ Mutation: {
+ CreateUser: async (resolve, root, args, context, info) => {
+ const result = await resolve(root, args, context, info)
+ await createOrUpdateLocations(args.id, args.locationName, context.driver)
+ return result
+ },
+ UpdateUser: async (resolve, root, args, context, info) => {
+ const result = await resolve(root, args, context, info)
+ await createOrUpdateLocations(args.id, args.locationName, context.driver)
+ return result
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/middleware/validation/index.js b/Human-Connection/backend/src/middleware/validation/index.js
new file mode 100644
index 000000000..cfc852dcb
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/validation/index.js
@@ -0,0 +1,31 @@
+import { UserInputError } from 'apollo-server'
+
+const USERNAME_MIN_LENGTH = 3
+
+const validateUsername = async (resolve, root, args, context, info) => {
+ if (!('name' in args) || (args.name && args.name.length >= USERNAME_MIN_LENGTH)) {
+ /* eslint-disable-next-line no-return-await */
+ return await resolve(root, args, context, info)
+ } else {
+ throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} characters long!`)
+ }
+}
+
+const validateUrl = async (resolve, root, args, context, info) => {
+ const { url } = args
+ const isValid = url.match(/^(?:https?:\/\/)(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g)
+ if (isValid) {
+ /* eslint-disable-next-line no-return-await */
+ return await resolve(root, args, context, info)
+ } else {
+ throw new UserInputError('Input is not a URL')
+ }
+}
+
+export default {
+ Mutation: {
+ CreateUser: validateUsername,
+ UpdateUser: validateUsername,
+ CreateSocialMedia: validateUrl,
+ },
+}
diff --git a/Human-Connection/backend/src/middleware/xssMiddleware.js b/Human-Connection/backend/src/middleware/xssMiddleware.js
new file mode 100644
index 000000000..06aa5b306
--- /dev/null
+++ b/Human-Connection/backend/src/middleware/xssMiddleware.js
@@ -0,0 +1,154 @@
+import walkRecursive from '../helpers/walkRecursive'
+// import { getByDot, setByDot, getItems, replaceItems } from 'feathers-hooks-common'
+import sanitizeHtml from 'sanitize-html'
+// import { isEmpty, intersection } from 'lodash'
+import cheerio from 'cheerio'
+import linkifyHtml from 'linkifyjs/html'
+
+const embedToAnchor = content => {
+ const $ = cheerio.load(content)
+ $('div[data-url-embed]').each((i, el) => {
+ let url = el.attribs['data-url-embed']
+ let aTag = $(`${url}`)
+ $(el).replaceWith(aTag)
+ })
+ return $('body').html()
+}
+
+function clean(dirty) {
+ if (!dirty) {
+ return dirty
+ }
+
+ // Convert embeds to a-tags
+ dirty = embedToAnchor(dirty)
+ dirty = linkifyHtml(dirty)
+ dirty = sanitizeHtml(dirty, {
+ allowedTags: [
+ 'iframe',
+ 'img',
+ 'p',
+ 'h3',
+ 'h4',
+ 'br',
+ 'hr',
+ 'b',
+ 'i',
+ 'em',
+ 'strong',
+ 'a',
+ 'pre',
+ 'ul',
+ 'li',
+ 'ol',
+ 's',
+ 'strike',
+ 'span',
+ 'blockquote',
+ ],
+ allowedAttributes: {
+ a: ['href', 'class', 'target', 'data-*', 'contenteditable'],
+ span: ['contenteditable', 'class', 'data-*'],
+ img: ['src'],
+ iframe: ['src', 'class', 'frameborder', 'allowfullscreen'],
+ },
+ allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'],
+ parser: {
+ lowerCaseTags: true,
+ },
+ transformTags: {
+ iframe: function(tagName, attribs) {
+ return {
+ tagName: 'a',
+ text: attribs.src,
+ attribs: {
+ href: attribs.src,
+ target: '_blank',
+ 'data-url-embed': '',
+ },
+ }
+ },
+ h1: 'h3',
+ h2: 'h3',
+ h3: 'h3',
+ h4: 'h4',
+ h5: 'strong',
+ i: 'em',
+ a: function(tagName, attribs) {
+ return {
+ tagName: 'a',
+ attribs: {
+ href: attribs.href,
+ target: '_blank',
+ rel: 'noopener noreferrer nofollow',
+ },
+ }
+ },
+ b: 'strong',
+ s: 'strike',
+ img: function(tagName, attribs) {
+ let src = attribs.src
+
+ if (!src) {
+ // remove broken images
+ return {}
+ }
+
+ // if (isEmpty(hook.result)) {
+ // const config = hook.app.get('thumbor')
+ // if (config && src.indexOf(config < 0)) {
+ // // download image
+ // // const ThumborUrlHelper = require('../helper/thumbor-helper')
+ // // const Thumbor = new ThumborUrlHelper(config.key || null, config.url || null)
+ // // src = Thumbor
+ // // .setImagePath(src)
+ // // .buildUrl('740x0')
+ // }
+ // }
+ return {
+ tagName: 'img',
+ attribs: {
+ // TODO: use environment variables
+ src: `http://localhost:3050/images?url=${src}`,
+ },
+ }
+ },
+ },
+ })
+
+ // remove empty html tags and duplicated linebreaks and returns
+ dirty = dirty
+ // remove all tags with "space only"
+ .replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '')
+ // remove all iframes
+ .replace(/(
')
+ // remove additional linebreaks inside p tags
+ .replace(/<[a-z-]+>(<[a-z-]+>)*\s*(
\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim, '')
+ // remove additional linebreaks when first child inside p tags
+ .replace(/
(\s*
\s*)+/gim, '
')
+ // remove additional linebreaks when last child inside p tags
+ .replace(/(\s*
\s*)+<\/p+>/gim, '
')
+ return dirty
+}
+
+const fields = ['content', 'contentExcerpt']
+
+export default {
+ Mutation: async (resolve, root, args, context, info) => {
+ args = walkRecursive(args, fields, clean)
+ const result = await resolve(root, args, context, info)
+ return result
+ },
+ Query: async (resolve, root, args, context, info) => {
+ const result = await resolve(root, args, context, info)
+ return walkRecursive(result, fields, clean)
+ },
+}
diff --git a/Human-Connection/backend/src/mocks/index.js b/Human-Connection/backend/src/mocks/index.js
new file mode 100644
index 000000000..7b453c8c6
--- /dev/null
+++ b/Human-Connection/backend/src/mocks/index.js
@@ -0,0 +1,14 @@
+import faker from 'faker'
+
+export default {
+ User: () => ({
+ name: () => `${faker.name.firstName()} ${faker.name.lastName()}`,
+ email: () => `${faker.internet.email()}`,
+ }),
+ Post: () => ({
+ title: () => faker.lorem.lines(1),
+ slug: () => faker.lorem.slug(3),
+ content: () => faker.lorem.paragraphs(5),
+ contentExcerpt: () => faker.lorem.paragraphs(1),
+ }),
+}
diff --git a/Human-Connection/backend/src/schema/index.js b/Human-Connection/backend/src/schema/index.js
new file mode 100644
index 000000000..d294d8aba
--- /dev/null
+++ b/Human-Connection/backend/src/schema/index.js
@@ -0,0 +1,24 @@
+import { makeAugmentedSchema } from 'neo4j-graphql-js'
+import CONFIG from './../config'
+import applyScalars from './../bootstrap/scalars'
+import applyDirectives from './../bootstrap/directives'
+import typeDefs from './types'
+import resolvers from './resolvers'
+
+export default applyScalars(
+ applyDirectives(
+ makeAugmentedSchema({
+ typeDefs,
+ resolvers,
+ config: {
+ query: {
+ exclude: ['Notfication', 'Statistics', 'LoggedInUser'],
+ },
+ mutation: {
+ exclude: ['Notfication', 'Statistics', 'LoggedInUser'],
+ },
+ debug: CONFIG.DEBUG,
+ },
+ }),
+ ),
+)
diff --git a/Human-Connection/backend/src/schema/resolvers/badges.spec.js b/Human-Connection/backend/src/schema/resolvers/badges.spec.js
new file mode 100644
index 000000000..a0dbafe00
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/badges.spec.js
@@ -0,0 +1,200 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../seed/factories'
+import { host, login } from '../../jest/helpers'
+
+const factory = Factory()
+let client
+
+describe('badges', () => {
+ beforeEach(async () => {
+ await factory.create('User', {
+ email: 'user@example.org',
+ role: 'user',
+ password: '1234',
+ })
+ await factory.create('User', {
+ id: 'u2',
+ role: 'moderator',
+ email: 'moderator@example.org',
+ })
+ await factory.create('User', {
+ id: 'u3',
+ role: 'admin',
+ email: 'admin@example.org',
+ })
+ })
+
+ afterEach(async () => {
+ await factory.cleanDatabase()
+ })
+
+ describe('CreateBadge', () => {
+ const variables = {
+ id: 'b1',
+ key: 'indiegogo_en_racoon',
+ type: 'crowdfunding',
+ status: 'permanent',
+ icon: '/img/badges/indiegogo_en_racoon.svg',
+ }
+
+ const mutation = `
+ mutation(
+ $id: ID
+ $key: String!
+ $type: BadgeType!
+ $status: BadgeStatus!
+ $icon: String!
+ ) {
+ CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
+ id,
+ key,
+ type,
+ status,
+ icon
+ }
+ }
+ `
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated admin', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'admin@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+ it('creates a badge', async () => {
+ const expected = {
+ CreateBadge: {
+ icon: '/img/badges/indiegogo_en_racoon.svg',
+ id: 'b1',
+ key: 'indiegogo_en_racoon',
+ status: 'permanent',
+ type: 'crowdfunding',
+ },
+ }
+ await expect(client.request(mutation, variables)).resolves.toEqual(expected)
+ })
+ })
+
+ describe('authenticated moderator', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'moderator@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('throws authorization error', async () => {
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+ })
+
+ describe('UpdateBadge', () => {
+ beforeEach(async () => {
+ await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
+ await factory.create('Badge', { id: 'b1' })
+ })
+ const variables = {
+ id: 'b1',
+ key: 'whatever',
+ }
+
+ const mutation = `
+ mutation($id: ID!, $key: String!) {
+ UpdateBadge(id: $id, key: $key) {
+ id
+ key
+ }
+ }
+ `
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated moderator', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'moderator@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('throws authorization error', async () => {
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated admin', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'admin@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+ it('updates a badge', async () => {
+ const expected = {
+ UpdateBadge: {
+ id: 'b1',
+ key: 'whatever',
+ },
+ }
+ await expect(client.request(mutation, variables)).resolves.toEqual(expected)
+ })
+ })
+ })
+
+ describe('DeleteBadge', () => {
+ beforeEach(async () => {
+ await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
+ await factory.create('Badge', { id: 'b1' })
+ })
+ const variables = {
+ id: 'b1',
+ }
+
+ const mutation = `
+ mutation($id: ID!) {
+ DeleteBadge(id: $id) {
+ id
+ }
+ }
+ `
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated moderator', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'moderator@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('throws authorization error', async () => {
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated admin', () => {
+ beforeEach(async () => {
+ const headers = await login({ email: 'admin@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+ it('deletes a badge', async () => {
+ const expected = {
+ DeleteBadge: {
+ id: 'b1',
+ },
+ }
+ await expect(client.request(mutation, variables)).resolves.toEqual(expected)
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/comments.js b/Human-Connection/backend/src/schema/resolvers/comments.js
new file mode 100644
index 000000000..7aef63c59
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/comments.js
@@ -0,0 +1,80 @@
+import { neo4jgraphql } from 'neo4j-graphql-js'
+import { UserInputError } from 'apollo-server'
+
+const COMMENT_MIN_LENGTH = 1
+const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
+
+export default {
+ Mutation: {
+ CreateComment: async (object, params, context, resolveInfo) => {
+ const content = params.content.replace(/<(?:.|\n)*?>/gm, '').trim()
+ const { postId } = params
+ // Adding relationship from comment to post by passing in the postId,
+ // but we do not want to create the comment with postId as an attribute
+ // because we use relationships for this. So, we are deleting it from params
+ // before comment creation.
+ delete params.postId
+
+ if (!params.content || content.length < COMMENT_MIN_LENGTH) {
+ throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
+ }
+
+ const session = context.driver.session()
+ const postQueryRes = await session.run(
+ `
+ MATCH (post:Post {id: $postId})
+ RETURN post`,
+ {
+ postId,
+ },
+ )
+ const [post] = postQueryRes.records.map(record => {
+ return record.get('post')
+ })
+
+ if (!post) {
+ throw new UserInputError(NO_POST_ERR_MESSAGE)
+ }
+ const commentWithoutRelationships = await neo4jgraphql(
+ object,
+ params,
+ context,
+ resolveInfo,
+ false,
+ )
+
+ let transactionRes = await session.run(
+ `
+ MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId}), (author:User {id: $userId})
+ MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
+ RETURN comment, author`,
+ {
+ userId: context.user.id,
+ postId,
+ commentId: commentWithoutRelationships.id,
+ },
+ )
+
+ const [commentWithAuthor] = transactionRes.records.map(record => {
+ return {
+ comment: record.get('comment'),
+ author: record.get('author'),
+ }
+ })
+
+ const { comment, author } = commentWithAuthor
+
+ const commentReturnedWithAuthor = {
+ ...comment.properties,
+ author: author.properties,
+ }
+ session.close()
+ return commentReturnedWithAuthor
+ },
+ DeleteComment: async (object, params, context, resolveInfo) => {
+ const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
+
+ return comment
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/comments.spec.js b/Human-Connection/backend/src/schema/resolvers/comments.spec.js
new file mode 100644
index 000000000..07462ed49
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/comments.spec.js
@@ -0,0 +1,275 @@
+import gql from 'graphql-tag'
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../seed/factories'
+import { host, login } from '../../jest/helpers'
+
+const factory = Factory()
+let client
+let createCommentVariables
+let createPostVariables
+let createCommentVariablesSansPostId
+let createCommentVariablesWithNonExistentPost
+
+beforeEach(async () => {
+ await factory.create('User', {
+ email: 'test@example.org',
+ password: '1234',
+ })
+})
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('CreateComment', () => {
+ const createCommentMutation = gql`
+ mutation($postId: ID!, $content: String!) {
+ CreateComment(postId: $postId, content: $content) {
+ id
+ content
+ }
+ }
+ `
+ const createPostMutation = gql`
+ mutation($id: ID!, $title: String!, $content: String!) {
+ CreatePost(id: $id, title: $title, content: $content) {
+ id
+ }
+ }
+ `
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ createCommentVariables = {
+ postId: 'p1',
+ content: "I'm not authorised to comment",
+ }
+ client = new GraphQLClient(host)
+ await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
+ 'Not Authorised',
+ )
+ })
+ })
+
+ describe('authenticated', () => {
+ let headers
+ beforeEach(async () => {
+ headers = await login({
+ email: 'test@example.org',
+ password: '1234',
+ })
+ client = new GraphQLClient(host, {
+ headers,
+ })
+ createCommentVariables = {
+ postId: 'p1',
+ content: "I'm authorised to comment",
+ }
+ createPostVariables = {
+ id: 'p1',
+ title: 'post to comment on',
+ content: 'please comment on me',
+ }
+ await client.request(createPostMutation, createPostVariables)
+ })
+
+ it('creates a comment', async () => {
+ const expected = {
+ CreateComment: {
+ content: "I'm authorised to comment",
+ },
+ }
+
+ await expect(
+ client.request(createCommentMutation, createCommentVariables),
+ ).resolves.toMatchObject(expected)
+ })
+
+ it('assigns the authenticated user as author', async () => {
+ await client.request(createCommentMutation, createCommentVariables)
+
+ const { User } = await client.request(gql`
+ {
+ User(email: "test@example.org") {
+ comments {
+ content
+ }
+ }
+ }
+ `)
+
+ expect(User).toEqual([
+ {
+ comments: [
+ {
+ content: "I'm authorised to comment",
+ },
+ ],
+ },
+ ])
+ })
+
+ it('throw an error if an empty string is sent from the editor as content', async () => {
+ createCommentVariables = {
+ postId: 'p1',
+ content: '',
+ }
+
+ await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
+ 'Comment must be at least 1 character long!',
+ )
+ })
+
+ it('throws an error if a comment sent from the editor does not contain a single character', async () => {
+ createCommentVariables = {
+ postId: 'p1',
+ content: '
',
+ }
+
+ await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
+ 'Comment must be at least 1 character long!',
+ )
+ })
+
+ it('throws an error if postId is sent as an empty string', async () => {
+ createCommentVariables = {
+ postId: 'p1',
+ content: '',
+ }
+
+ await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
+ 'Comment must be at least 1 character long!',
+ )
+ })
+
+ it('throws an error if content is sent as an string of empty characters', async () => {
+ createCommentVariables = {
+ postId: 'p1',
+ content: ' ',
+ }
+
+ await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
+ 'Comment must be at least 1 character long!',
+ )
+ })
+
+ it('throws an error if postId is sent as an empty string', async () => {
+ createCommentVariablesSansPostId = {
+ postId: '',
+ content: 'this comment should not be created',
+ }
+
+ await expect(
+ client.request(createCommentMutation, createCommentVariablesSansPostId),
+ ).rejects.toThrow('Comment cannot be created without a post!')
+ })
+
+ it('throws an error if postId is sent as an string of empty characters', async () => {
+ createCommentVariablesSansPostId = {
+ postId: ' ',
+ content: 'this comment should not be created',
+ }
+
+ await expect(
+ client.request(createCommentMutation, createCommentVariablesSansPostId),
+ ).rejects.toThrow('Comment cannot be created without a post!')
+ })
+
+ it('throws an error if the post does not exist in the database', async () => {
+ createCommentVariablesWithNonExistentPost = {
+ postId: 'p2',
+ content: "comment should not be created cause the post doesn't exist",
+ }
+
+ await expect(
+ client.request(createCommentMutation, createCommentVariablesWithNonExistentPost),
+ ).rejects.toThrow('Comment cannot be created without a post!')
+ })
+ })
+})
+
+describe('DeleteComment', () => {
+ const deleteCommentMutation = gql`
+ mutation($id: ID!) {
+ DeleteComment(id: $id) {
+ id
+ }
+ }
+ `
+
+ let deleteCommentVariables = {
+ id: 'c1',
+ }
+
+ beforeEach(async () => {
+ const asAuthor = Factory()
+ await asAuthor.create('User', {
+ email: 'author@example.org',
+ password: '1234',
+ })
+ await asAuthor.authenticateAs({
+ email: 'author@example.org',
+ password: '1234',
+ })
+ await asAuthor.create('Post', {
+ id: 'p1',
+ content: 'Post to be commented',
+ })
+ await asAuthor.create('Comment', {
+ id: 'c1',
+ postId: 'p1',
+ content: 'Comment to be deleted',
+ })
+ })
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
+ 'Not Authorised',
+ )
+ })
+ })
+
+ describe('authenticated but not the author', () => {
+ beforeEach(async () => {
+ let headers
+ headers = await login({
+ email: 'test@example.org',
+ password: '1234',
+ })
+ client = new GraphQLClient(host, {
+ headers,
+ })
+ })
+
+ it('throws authorization error', async () => {
+ await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
+ 'Not Authorised',
+ )
+ })
+ })
+
+ describe('authenticated as author', () => {
+ beforeEach(async () => {
+ let headers
+ headers = await login({
+ email: 'author@example.org',
+ password: '1234',
+ })
+ client = new GraphQLClient(host, {
+ headers,
+ })
+ })
+
+ it('deletes the comment', async () => {
+ const expected = {
+ DeleteComment: {
+ id: 'c1',
+ },
+ }
+ await expect(client.request(deleteCommentMutation, deleteCommentVariables)).resolves.toEqual(
+ expected,
+ )
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/fileUpload/index.js b/Human-Connection/backend/src/schema/resolvers/fileUpload/index.js
new file mode 100644
index 000000000..fa78238c3
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/fileUpload/index.js
@@ -0,0 +1,26 @@
+import { createWriteStream } from 'fs'
+import path from 'path'
+import slug from 'slug'
+
+const storeUpload = ({ createReadStream, fileLocation }) =>
+ new Promise((resolve, reject) =>
+ createReadStream()
+ .pipe(createWriteStream(`public${fileLocation}`))
+ .on('finish', resolve)
+ .on('error', reject),
+ )
+
+export default async function fileUpload(params, { file, url }, uploadCallback = storeUpload) {
+ const upload = params[file]
+ if (upload) {
+ const { createReadStream, filename } = await upload
+ const { name } = path.parse(filename)
+ const fileLocation = `/uploads/${Date.now()}-${slug(name)}`
+ await uploadCallback({ createReadStream, fileLocation })
+ delete params[file]
+
+ params[url] = fileLocation
+ }
+
+ return params
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/fileUpload/spec.js b/Human-Connection/backend/src/schema/resolvers/fileUpload/spec.js
new file mode 100644
index 000000000..5767d6457
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/fileUpload/spec.js
@@ -0,0 +1,65 @@
+import fileUpload from '.'
+
+describe('fileUpload', () => {
+ let params
+ let uploadCallback
+
+ beforeEach(() => {
+ params = {
+ uploadAttribute: {
+ filename: 'avatar.jpg',
+ mimetype: 'image/jpeg',
+ encoding: '7bit',
+ createReadStream: jest.fn(),
+ },
+ }
+ uploadCallback = jest.fn()
+ })
+
+ 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(/^\/uploads\/\d+-avatar$/)
+ })
+
+ it('uses the name without file ending', async () => {
+ params.uploadAttribute.filename = 'somePng.png'
+ await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
+ expect(params.attribute).toMatch(/^\/uploads\/\d+-somePng/)
+ })
+
+ it('creates a url safe name', async () => {
+ params.uploadAttribute.filename =
+ '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg?foo- bar'
+ await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
+ expect(params.attribute).toMatch(/^\/uploads\/\d+-foo-bar-avatar$/)
+ })
+
+ 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,
+ )
+
+ await new Promise(resolve => setTimeout(resolve, 1000))
+ const { attribute: second } = await fileUpload(
+ {
+ ...params,
+ },
+ { file: 'uploadAttribute', url: 'attribute' },
+ uploadCallback,
+ )
+ expect(first).not.toEqual(second)
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/follow.js b/Human-Connection/backend/src/schema/resolvers/follow.js
new file mode 100644
index 000000000..4e9a3b27d
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/follow.js
@@ -0,0 +1,51 @@
+export default {
+ Mutation: {
+ follow: async (_object, params, context, _resolveInfo) => {
+ const { id, type } = params
+
+ const session = context.driver.session()
+ let transactionRes = await session.run(
+ `MATCH (node {id: $id}), (user:User {id: $userId})
+ WHERE $type IN labels(node) AND NOT $id = $userId
+ MERGE (user)-[relation:FOLLOWS]->(node)
+ RETURN COUNT(relation) > 0 as isFollowed`,
+ {
+ id,
+ type,
+ userId: context.user.id,
+ },
+ )
+
+ const [isFollowed] = transactionRes.records.map(record => {
+ return record.get('isFollowed')
+ })
+
+ session.close()
+
+ return isFollowed
+ },
+
+ unfollow: async (_object, params, context, _resolveInfo) => {
+ const { id, type } = params
+ const session = context.driver.session()
+
+ let transactionRes = await session.run(
+ `MATCH (user:User {id: $userId})-[relation:FOLLOWS]->(node {id: $id})
+ WHERE $type IN labels(node)
+ DELETE relation
+ RETURN COUNT(relation) > 0 as isFollowed`,
+ {
+ id,
+ type,
+ userId: context.user.id,
+ },
+ )
+ const [isFollowed] = transactionRes.records.map(record => {
+ return record.get('isFollowed')
+ })
+ session.close()
+
+ return isFollowed
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/follow.spec.js b/Human-Connection/backend/src/schema/resolvers/follow.spec.js
new file mode 100644
index 000000000..d29e17938
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/follow.spec.js
@@ -0,0 +1,125 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../seed/factories'
+import { host, login } from '../../jest/helpers'
+
+const factory = Factory()
+let clientUser1
+let headersUser1
+
+const mutationFollowUser = id => `
+ mutation {
+ follow(id: "${id}", type: User)
+ }
+`
+const mutationUnfollowUser = id => `
+ mutation {
+ unfollow(id: "${id}", type: User)
+ }
+`
+
+beforeEach(async () => {
+ await factory.create('User', {
+ id: 'u1',
+ email: 'test@example.org',
+ password: '1234',
+ })
+ await factory.create('User', {
+ id: 'u2',
+ email: 'test2@example.org',
+ password: '1234',
+ })
+
+ headersUser1 = await login({ email: 'test@example.org', password: '1234' })
+ clientUser1 = new GraphQLClient(host, { headers: headersUser1 })
+})
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('follow', () => {
+ describe('follow user', () => {
+ describe('unauthenticated follow', () => {
+ it('throws authorization error', async () => {
+ let client
+ client = new GraphQLClient(host)
+ await expect(client.request(mutationFollowUser('u2'))).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ it('I can follow another user', async () => {
+ const res = await clientUser1.request(mutationFollowUser('u2'))
+ const expected = {
+ follow: true,
+ }
+ expect(res).toMatchObject(expected)
+
+ const { User } = await clientUser1.request(`{
+ User(id: "u2") {
+ followedBy { id }
+ followedByCurrentUser
+ }
+ }`)
+ const expected2 = {
+ followedBy: [{ id: 'u1' }],
+ followedByCurrentUser: true,
+ }
+ expect(User[0]).toMatchObject(expected2)
+ })
+
+ it('I can`t follow myself', async () => {
+ const res = await clientUser1.request(mutationFollowUser('u1'))
+ const expected = {
+ follow: false,
+ }
+ expect(res).toMatchObject(expected)
+
+ const { User } = await clientUser1.request(`{
+ User(id: "u1") {
+ followedBy { id }
+ followedByCurrentUser
+ }
+ }`)
+ const expected2 = {
+ followedBy: [],
+ followedByCurrentUser: false,
+ }
+ expect(User[0]).toMatchObject(expected2)
+ })
+ })
+ describe('unfollow user', () => {
+ describe('unauthenticated follow', () => {
+ it('throws authorization error', async () => {
+ // follow
+ await clientUser1.request(mutationFollowUser('u2'))
+ // unfollow
+ let client
+ client = new GraphQLClient(host)
+ await expect(client.request(mutationUnfollowUser('u2'))).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ it('I can unfollow a user', async () => {
+ // follow
+ await clientUser1.request(mutationFollowUser('u2'))
+ // unfollow
+ const expected = {
+ unfollow: true,
+ }
+ const res = await clientUser1.request(mutationUnfollowUser('u2'))
+ expect(res).toMatchObject(expected)
+
+ const { User } = await clientUser1.request(`{
+ User(id: "u2") {
+ followedBy { id }
+ followedByCurrentUser
+ }
+ }`)
+ const expected2 = {
+ followedBy: [],
+ followedByCurrentUser: false,
+ }
+ expect(User[0]).toMatchObject(expected2)
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/index.js b/Human-Connection/backend/src/schema/resolvers/index.js
new file mode 100644
index 000000000..3d3a91d68
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/index.js
@@ -0,0 +1,5 @@
+import path from 'path'
+import { fileLoader, mergeResolvers } from 'merge-graphql-schemas'
+
+const resolversArray = fileLoader(path.join(__dirname, './!(*.spec).js'))
+export default mergeResolvers(resolversArray)
diff --git a/Human-Connection/backend/src/schema/resolvers/moderation.js b/Human-Connection/backend/src/schema/resolvers/moderation.js
new file mode 100644
index 000000000..d61df7545
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/moderation.js
@@ -0,0 +1,41 @@
+export default {
+ Mutation: {
+ disable: async (object, params, { user, driver }) => {
+ const { id } = params
+ const { id: userId } = user
+ const cypher = `
+ MATCH (u:User {id: $userId})
+ MATCH (resource {id: $id})
+ WHERE resource:User OR resource:Comment OR resource:Post
+ SET resource.disabled = true
+ MERGE (resource)<-[:DISABLED]-(u)
+ RETURN resource {.id}
+ `
+ const session = driver.session()
+ const res = await session.run(cypher, { id, userId })
+ session.close()
+ const [resource] = res.records.map(record => {
+ return record.get('resource')
+ })
+ if (!resource) return null
+ return resource.id
+ },
+ enable: async (object, params, { user, driver }) => {
+ const { id } = params
+ const cypher = `
+ MATCH (resource {id: $id})<-[d:DISABLED]-()
+ SET resource.disabled = false
+ DELETE d
+ RETURN resource {.id}
+ `
+ const session = driver.session()
+ const res = await session.run(cypher, { id })
+ session.close()
+ const [resource] = res.records.map(record => {
+ return record.get('resource')
+ })
+ if (!resource) return null
+ return resource.id
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/moderation.spec.js b/Human-Connection/backend/src/schema/resolvers/moderation.spec.js
new file mode 100644
index 000000000..b1dec603b
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/moderation.spec.js
@@ -0,0 +1,410 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../seed/factories'
+import { host, login } from '../../jest/helpers'
+
+const factory = Factory()
+let client
+
+const setupAuthenticateClient = params => {
+ const authenticateClient = async () => {
+ await factory.create('User', params)
+ const headers = await login(params)
+ client = new GraphQLClient(host, { headers })
+ }
+ return authenticateClient
+}
+
+let createResource
+let authenticateClient
+let createPostVariables
+let createCommentVariables
+
+beforeEach(() => {
+ createResource = () => {}
+ authenticateClient = () => {
+ client = new GraphQLClient(host)
+ }
+})
+
+const setup = async () => {
+ await createResource()
+ await authenticateClient()
+}
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('disable', () => {
+ const mutation = `
+ mutation($id: ID!) {
+ disable(id: $id)
+ }
+ `
+ let variables
+
+ beforeEach(() => {
+ // our defaul set of variables
+ variables = {
+ id: 'blabla',
+ }
+ })
+
+ const action = async () => {
+ return client.request(mutation, variables)
+ }
+
+ it('throws authorization error', async () => {
+ await setup()
+ await expect(action()).rejects.toThrow('Not Authorised')
+ })
+
+ describe('authenticated', () => {
+ beforeEach(() => {
+ authenticateClient = setupAuthenticateClient({
+ email: 'user@example.org',
+ password: '1234',
+ })
+ })
+
+ it('throws authorization error', async () => {
+ await setup()
+ await expect(action()).rejects.toThrow('Not Authorised')
+ })
+
+ describe('as moderator', () => {
+ beforeEach(() => {
+ authenticateClient = setupAuthenticateClient({
+ id: 'u7',
+ email: 'moderator@example.org',
+ password: '1234',
+ role: 'moderator',
+ })
+ })
+
+ describe('on something that is not a (Comment|Post|User) ', () => {
+ beforeEach(async () => {
+ variables = {
+ id: 't23',
+ }
+ createResource = () => {
+ return Promise.all([factory.create('Tag', { id: 't23' })])
+ }
+ })
+
+ it('returns null', async () => {
+ const expected = { disable: null }
+ await setup()
+ await expect(action()).resolves.toEqual(expected)
+ })
+ })
+
+ describe('on a comment', () => {
+ beforeEach(async () => {
+ variables = {
+ id: 'c47',
+ }
+ createPostVariables = {
+ id: 'p3',
+ title: 'post to comment on',
+ content: 'please comment on me',
+ }
+ createCommentVariables = {
+ id: 'c47',
+ postId: 'p3',
+ content: 'this comment was created for this post',
+ }
+ createResource = async () => {
+ await factory.create('User', {
+ id: 'u45',
+ email: 'commenter@example.org',
+ password: '1234',
+ })
+ const asAuthenticatedUser = await factory.authenticateAs({
+ email: 'commenter@example.org',
+ password: '1234',
+ })
+ await asAuthenticatedUser.create('Post', createPostVariables)
+ await asAuthenticatedUser.create('Comment', createCommentVariables)
+ }
+ })
+
+ it('returns disabled resource id', async () => {
+ const expected = { disable: 'c47' }
+ await setup()
+ await expect(action()).resolves.toEqual(expected)
+ })
+
+ it('changes .disabledBy', async () => {
+ const before = { Comment: [{ id: 'c47', disabledBy: null }] }
+ const expected = { Comment: [{ id: 'c47', disabledBy: { id: 'u7' } }] }
+
+ await setup()
+ await expect(client.request('{ Comment { id, disabledBy { id } } }')).resolves.toEqual(
+ before,
+ )
+ await action()
+ await expect(
+ client.request('{ Comment(disabled: true) { id, disabledBy { id } } }'),
+ ).resolves.toEqual(expected)
+ })
+
+ it('updates .disabled on comment', async () => {
+ const before = { Comment: [{ id: 'c47', disabled: false }] }
+ const expected = { Comment: [{ id: 'c47', disabled: true }] }
+
+ await setup()
+ await expect(client.request('{ Comment { id disabled } }')).resolves.toEqual(before)
+ await action()
+ await expect(
+ client.request('{ Comment(disabled: true) { id disabled } }'),
+ ).resolves.toEqual(expected)
+ })
+ })
+
+ describe('on a post', () => {
+ beforeEach(async () => {
+ variables = {
+ id: 'p9',
+ }
+
+ createResource = async () => {
+ await factory.create('User', { email: 'author@example.org', password: '1234' })
+ await factory.authenticateAs({ email: 'author@example.org', password: '1234' })
+ await factory.create('Post', {
+ id: 'p9', // that's the ID we will look for
+ })
+ }
+ })
+
+ it('returns disabled resource id', async () => {
+ const expected = { disable: 'p9' }
+ await setup()
+ await expect(action()).resolves.toEqual(expected)
+ })
+
+ it('changes .disabledBy', async () => {
+ const before = { Post: [{ id: 'p9', disabledBy: null }] }
+ const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] }
+
+ await setup()
+ await expect(client.request('{ Post { id, disabledBy { id } } }')).resolves.toEqual(
+ before,
+ )
+ await action()
+ await expect(
+ client.request('{ Post(disabled: true) { id, disabledBy { id } } }'),
+ ).resolves.toEqual(expected)
+ })
+
+ it('updates .disabled on post', async () => {
+ const before = { Post: [{ id: 'p9', disabled: false }] }
+ const expected = { Post: [{ id: 'p9', disabled: true }] }
+
+ await setup()
+ await expect(client.request('{ Post { id disabled } }')).resolves.toEqual(before)
+ await action()
+ await expect(client.request('{ Post(disabled: true) { id disabled } }')).resolves.toEqual(
+ expected,
+ )
+ })
+ })
+ })
+ })
+})
+
+describe('enable', () => {
+ const mutation = `
+ mutation($id: ID!) {
+ enable(id: $id)
+ }
+ `
+ let variables
+
+ const action = async () => {
+ return client.request(mutation, variables)
+ }
+
+ beforeEach(() => {
+ // our defaul set of variables
+ variables = {
+ id: 'blabla',
+ }
+ })
+
+ it('throws authorization error', async () => {
+ await setup()
+ await expect(action()).rejects.toThrow('Not Authorised')
+ })
+
+ describe('authenticated', () => {
+ beforeEach(() => {
+ authenticateClient = setupAuthenticateClient({
+ email: 'user@example.org',
+ password: '1234',
+ })
+ })
+
+ it('throws authorization error', async () => {
+ await setup()
+ await expect(action()).rejects.toThrow('Not Authorised')
+ })
+
+ describe('as moderator', () => {
+ beforeEach(async () => {
+ authenticateClient = setupAuthenticateClient({
+ role: 'moderator',
+ email: 'someUser@example.org',
+ password: '1234',
+ })
+ })
+
+ describe('on something that is not a (Comment|Post|User) ', () => {
+ beforeEach(async () => {
+ variables = {
+ id: 't23',
+ }
+ createResource = () => {
+ // we cannot create a :DISABLED relationship here
+ return Promise.all([factory.create('Tag', { id: 't23' })])
+ }
+ })
+
+ it('returns null', async () => {
+ const expected = { enable: null }
+ await setup()
+ await expect(action()).resolves.toEqual(expected)
+ })
+ })
+
+ describe('on a comment', () => {
+ beforeEach(async () => {
+ variables = {
+ id: 'c456',
+ }
+ createPostVariables = {
+ id: 'p9',
+ title: 'post to comment on',
+ content: 'please comment on me',
+ }
+ createCommentVariables = {
+ id: 'c456',
+ postId: 'p9',
+ content: 'this comment was created for this post',
+ }
+ createResource = async () => {
+ await factory.create('User', {
+ id: 'u123',
+ email: 'author@example.org',
+ password: '1234',
+ })
+ const asAuthenticatedUser = await factory.authenticateAs({
+ email: 'author@example.org',
+ password: '1234',
+ })
+ await asAuthenticatedUser.create('Post', createPostVariables)
+ await asAuthenticatedUser.create('Comment', createCommentVariables)
+
+ const disableMutation = `
+ mutation {
+ disable(id: "c456")
+ }
+ `
+ await factory.mutate(disableMutation) // that's we want to delete
+ }
+ })
+
+ it('returns disabled resource id', async () => {
+ const expected = { enable: 'c456' }
+ await setup()
+ await expect(action()).resolves.toEqual(expected)
+ })
+
+ it('changes .disabledBy', async () => {
+ const before = { Comment: [{ id: 'c456', disabledBy: { id: 'u123' } }] }
+ const expected = { Comment: [{ id: 'c456', disabledBy: null }] }
+
+ await setup()
+ await expect(
+ client.request('{ Comment(disabled: true) { id, disabledBy { id } } }'),
+ ).resolves.toEqual(before)
+ await action()
+ await expect(client.request('{ Comment { id, disabledBy { id } } }')).resolves.toEqual(
+ expected,
+ )
+ })
+
+ it('updates .disabled on post', async () => {
+ const before = { Comment: [{ id: 'c456', disabled: true }] }
+ const expected = { Comment: [{ id: 'c456', disabled: false }] }
+
+ await setup()
+ await expect(
+ client.request('{ Comment(disabled: true) { id disabled } }'),
+ ).resolves.toEqual(before)
+ await action() // this updates .disabled
+ await expect(client.request('{ Comment { id disabled } }')).resolves.toEqual(expected)
+ })
+ })
+
+ describe('on a post', () => {
+ beforeEach(async () => {
+ variables = {
+ id: 'p9',
+ }
+
+ createResource = async () => {
+ await factory.create('User', {
+ id: 'u123',
+ email: 'author@example.org',
+ password: '1234',
+ })
+ await factory.authenticateAs({ email: 'author@example.org', password: '1234' })
+ await factory.create('Post', {
+ id: 'p9', // that's the ID we will look for
+ })
+
+ const disableMutation = `
+ mutation {
+ disable(id: "p9")
+ }
+ `
+ await factory.mutate(disableMutation) // that's we want to delete
+ }
+ })
+
+ it('returns disabled resource id', async () => {
+ const expected = { enable: 'p9' }
+ await setup()
+ await expect(action()).resolves.toEqual(expected)
+ })
+
+ it('changes .disabledBy', async () => {
+ const before = { Post: [{ id: 'p9', disabledBy: { id: 'u123' } }] }
+ const expected = { Post: [{ id: 'p9', disabledBy: null }] }
+
+ await setup()
+ await expect(
+ client.request('{ Post(disabled: true) { id, disabledBy { id } } }'),
+ ).resolves.toEqual(before)
+ await action()
+ await expect(client.request('{ Post { id, disabledBy { id } } }')).resolves.toEqual(
+ expected,
+ )
+ })
+
+ it('updates .disabled on post', async () => {
+ const before = { Post: [{ id: 'p9', disabled: true }] }
+ const expected = { Post: [{ id: 'p9', disabled: false }] }
+
+ await setup()
+ await expect(client.request('{ Post(disabled: true) { id disabled } }')).resolves.toEqual(
+ before,
+ )
+ await action() // this updates .disabled
+ await expect(client.request('{ Post { id disabled } }')).resolves.toEqual(expected)
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/notifications.js b/Human-Connection/backend/src/schema/resolvers/notifications.js
new file mode 100644
index 000000000..ddc1985cf
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/notifications.js
@@ -0,0 +1,14 @@
+import { neo4jgraphql } from 'neo4j-graphql-js'
+
+export default {
+ Query: {
+ Notification: (object, params, context, resolveInfo) => {
+ return neo4jgraphql(object, params, context, resolveInfo, false)
+ },
+ },
+ Mutation: {
+ UpdateNotification: (object, params, context, resolveInfo) => {
+ return neo4jgraphql(object, params, context, resolveInfo, false)
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/notifications.spec.js b/Human-Connection/backend/src/schema/resolvers/notifications.spec.js
new file mode 100644
index 000000000..3876a4be3
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/notifications.spec.js
@@ -0,0 +1,178 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../seed/factories'
+import { host, login } from '../../jest/helpers'
+
+const factory = Factory()
+let client
+let userParams = {
+ id: 'you',
+ email: 'test@example.org',
+ password: '1234',
+}
+
+beforeEach(async () => {
+ await factory.create('User', userParams)
+})
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('Notification', () => {
+ const query = `{
+ Notification {
+ id
+ }
+ }`
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(query)).rejects.toThrow('Not Authorised')
+ })
+ })
+})
+
+describe('currentUser { notifications }', () => {
+ let variables = {}
+
+ describe('authenticated', () => {
+ let headers
+ beforeEach(async () => {
+ headers = await login({ email: 'test@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ describe('given some notifications', () => {
+ beforeEach(async () => {
+ const neighborParams = {
+ email: 'neighbor@example.org',
+ password: '1234',
+ id: 'neighbor',
+ }
+ await Promise.all([
+ factory.create('User', neighborParams),
+ factory.create('Notification', { id: 'not-for-you' }),
+ factory.create('Notification', { id: 'already-seen', read: true }),
+ ])
+ await factory.create('Notification', { id: 'unseen' })
+ await factory.authenticateAs(neighborParams)
+ await factory.create('Post', { id: 'p1' })
+ await Promise.all([
+ factory.relate('Notification', 'User', { from: 'not-for-you', to: 'neighbor' }),
+ factory.relate('Notification', 'Post', { from: 'p1', to: 'not-for-you' }),
+ factory.relate('Notification', 'User', { from: 'unseen', to: 'you' }),
+ factory.relate('Notification', 'Post', { from: 'p1', to: 'unseen' }),
+ factory.relate('Notification', 'User', { from: 'already-seen', to: 'you' }),
+ factory.relate('Notification', 'Post', { from: 'p1', to: 'already-seen' }),
+ ])
+ })
+
+ describe('filter for read: false', () => {
+ const query = `query($read: Boolean) {
+ currentUser {
+ notifications(read: $read, orderBy: createdAt_desc) {
+ id
+ post {
+ id
+ }
+ }
+ }
+ }`
+ let variables = { read: false }
+ it('returns only unread notifications of current user', async () => {
+ const expected = {
+ currentUser: {
+ notifications: [{ id: 'unseen', post: { id: 'p1' } }],
+ },
+ }
+ await expect(client.request(query, variables)).resolves.toEqual(expected)
+ })
+ })
+
+ describe('no filters', () => {
+ const query = `{
+ currentUser {
+ notifications(orderBy: createdAt_desc) {
+ id
+ post {
+ id
+ }
+ }
+ }
+ }`
+ it('returns all notifications of current user', async () => {
+ const expected = {
+ currentUser: {
+ notifications: [
+ { id: 'unseen', post: { id: 'p1' } },
+ { id: 'already-seen', post: { id: 'p1' } },
+ ],
+ },
+ }
+ await expect(client.request(query, variables)).resolves.toEqual(expected)
+ })
+ })
+ })
+ })
+})
+
+describe('UpdateNotification', () => {
+ const mutation = `mutation($id: ID!, $read: Boolean){
+ UpdateNotification(id: $id, read: $read) {
+ id read
+ }
+ }`
+ const variables = { id: 'to-be-updated', read: true }
+
+ describe('given a notifications', () => {
+ let headers
+
+ beforeEach(async () => {
+ const mentionedParams = {
+ id: 'mentioned-1',
+ email: 'mentioned@example.org',
+ password: '1234',
+ slug: 'mentioned',
+ }
+ await factory.create('User', mentionedParams)
+ await factory.create('Notification', { id: 'to-be-updated' })
+ await factory.authenticateAs(userParams)
+ await factory.create('Post', { id: 'p1' })
+ await Promise.all([
+ factory.relate('Notification', 'User', { from: 'to-be-updated', to: 'mentioned-1' }),
+ factory.relate('Notification', 'Post', { from: 'p1', to: 'to-be-updated' }),
+ ])
+ })
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated', () => {
+ beforeEach(async () => {
+ headers = await login({ email: 'test@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('throws authorization error', async () => {
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+
+ describe('and owner', () => {
+ beforeEach(async () => {
+ headers = await login({ email: 'mentioned@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('updates notification', async () => {
+ const expected = { UpdateNotification: { id: 'to-be-updated', read: true } }
+ await expect(client.request(mutation, variables)).resolves.toEqual(expected)
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/passwordReset.js b/Human-Connection/backend/src/schema/resolvers/passwordReset.js
new file mode 100644
index 000000000..13789662b
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/passwordReset.js
@@ -0,0 +1,72 @@
+import uuid from 'uuid/v4'
+import bcrypt from 'bcryptjs'
+import CONFIG from '../../config'
+import nodemailer from 'nodemailer'
+import { resetPasswordMail, wrongAccountMail } from './passwordReset/emailTemplates'
+
+const transporter = () => {
+ const configs = {
+ host: CONFIG.SMTP_HOST,
+ port: CONFIG.SMTP_PORT,
+ ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
+ secure: false, // true for 465, false for other ports
+ }
+ const { SMTP_USERNAME: user, SMTP_PASSWORD: pass } = CONFIG
+ if (user && pass) {
+ configs.auth = { user, pass }
+ }
+ return nodemailer.createTransport(configs)
+}
+
+export async function createPasswordReset(options) {
+ const { driver, code, email, issuedAt = new Date() } = options
+ const session = driver.session()
+ const cypher = `
+ MATCH (u:User) WHERE u.email = $email
+ CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL})
+ MERGE (u)-[:REQUESTED]->(pr)
+ RETURN u
+ `
+ const transactionRes = await session.run(cypher, {
+ issuedAt: issuedAt.toISOString(),
+ code,
+ email,
+ })
+ const users = transactionRes.records.map(record => record.get('u'))
+ session.close()
+ return users
+}
+
+export default {
+ Mutation: {
+ requestPasswordReset: async (_, { email }, { driver }) => {
+ const code = uuid().substring(0, 6)
+ const [user] = await createPasswordReset({ driver, code, email })
+ if (CONFIG.SMTP_HOST && CONFIG.SMTP_PORT) {
+ const name = (user && user.name) || ''
+ const mailTemplate = user ? resetPasswordMail : wrongAccountMail
+ await transporter().sendMail(mailTemplate({ email, code, name }))
+ }
+ return true
+ },
+ resetPassword: async (_, { email, code, newPassword }, { driver }) => {
+ const session = driver.session()
+ const stillValid = new Date()
+ stillValid.setDate(stillValid.getDate() - 1)
+ const newHashedPassword = await bcrypt.hashSync(newPassword, 10)
+ const cypher = `
+ MATCH (pr:PasswordReset {code: $code})
+ MATCH (u:User {email: $email})-[:REQUESTED]->(pr)
+ WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
+ SET pr.usedAt = datetime()
+ SET u.password = $newHashedPassword
+ RETURN pr
+ `
+ let transactionRes = await session.run(cypher, { stillValid, email, code, newHashedPassword })
+ const [reset] = transactionRes.records.map(record => record.get('pr'))
+ const result = !!(reset && reset.properties.usedAt)
+ session.close()
+ return result
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/passwordReset.spec.js b/Human-Connection/backend/src/schema/resolvers/passwordReset.spec.js
new file mode 100644
index 000000000..545945f51
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/passwordReset.spec.js
@@ -0,0 +1,180 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../seed/factories'
+import { host } from '../../jest/helpers'
+import { getDriver } from '../../bootstrap/neo4j'
+import { createPasswordReset } from './passwordReset'
+
+const factory = Factory()
+let client
+const driver = getDriver()
+
+const getAllPasswordResets = async () => {
+ const session = driver.session()
+ let transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r')
+ const resets = transactionRes.records.map(record => record.get('r'))
+ session.close()
+ return resets
+}
+
+describe('passwordReset', () => {
+ beforeEach(async () => {
+ client = new GraphQLClient(host)
+ await factory.create('User', {
+ email: 'user@example.org',
+ role: 'user',
+ password: '1234',
+ })
+ })
+
+ afterEach(async () => {
+ await factory.cleanDatabase()
+ })
+
+ describe('requestPasswordReset', () => {
+ const mutation = `mutation($email: String!) { requestPasswordReset(email: $email) }`
+
+ describe('with invalid email', () => {
+ const variables = { email: 'non-existent@example.org' }
+
+ it('resolves anyways', async () => {
+ await expect(client.request(mutation, variables)).resolves.toEqual({
+ requestPasswordReset: true,
+ })
+ })
+
+ it('creates no node', async () => {
+ await client.request(mutation, variables)
+ const resets = await getAllPasswordResets()
+ expect(resets).toHaveLength(0)
+ })
+ })
+
+ describe('with a valid email', () => {
+ const variables = { email: 'user@example.org' }
+
+ it('resolves', async () => {
+ await expect(client.request(mutation, variables)).resolves.toEqual({
+ requestPasswordReset: true,
+ })
+ })
+
+ it('creates node with label `PasswordReset`', async () => {
+ await client.request(mutation, variables)
+ const resets = await getAllPasswordResets()
+ expect(resets).toHaveLength(1)
+ })
+
+ it('creates a reset code', async () => {
+ await client.request(mutation, variables)
+ const resets = await getAllPasswordResets()
+ const [reset] = resets
+ const { code } = reset.properties
+ expect(code).toHaveLength(6)
+ })
+ })
+ })
+
+ describe('resetPassword', () => {
+ const setup = async (options = {}) => {
+ const { email = 'user@example.org', issuedAt = new Date(), code = 'abcdef' } = options
+
+ const session = driver.session()
+ await createPasswordReset({ driver, email, issuedAt, code })
+ session.close()
+ }
+
+ const mutation = `mutation($code: String!, $email: String!, $newPassword: String!) { resetPassword(code: $code, email: $email, newPassword: $newPassword) }`
+ let email = 'user@example.org'
+ let code = 'abcdef'
+ let newPassword = 'supersecret'
+ let variables
+
+ describe('invalid email', () => {
+ it('resolves to false', async () => {
+ await setup()
+ variables = { newPassword, email: 'non-existent@example.org', code }
+ await expect(client.request(mutation, variables)).resolves.toEqual({ resetPassword: false })
+ })
+ })
+
+ describe('valid email', () => {
+ describe('but invalid code', () => {
+ it('resolves to false', async () => {
+ await setup()
+ variables = { newPassword, email, code: 'slkdjf' }
+ await expect(client.request(mutation, variables)).resolves.toEqual({
+ resetPassword: false,
+ })
+ })
+ })
+
+ describe('and valid code', () => {
+ beforeEach(() => {
+ variables = {
+ newPassword,
+ email: 'user@example.org',
+ code: 'abcdef',
+ }
+ })
+
+ describe('and code not expired', () => {
+ beforeEach(async () => {
+ await setup()
+ })
+
+ it('resolves to true', async () => {
+ await expect(client.request(mutation, variables)).resolves.toEqual({
+ resetPassword: true,
+ })
+ })
+
+ it('updates PasswordReset `usedAt` property', async () => {
+ await client.request(mutation, variables)
+ const requests = await getAllPasswordResets()
+ const [request] = requests
+ const { usedAt } = request.properties
+ expect(usedAt).not.toBeFalsy()
+ })
+
+ it('updates password of the user', async () => {
+ await client.request(mutation, variables)
+ const checkLoginMutation = `
+ mutation($email: String!, $password: String!) {
+ login(email: $email, password: $password)
+ }
+ `
+ const expected = expect.objectContaining({ login: expect.any(String) })
+ await expect(
+ client.request(checkLoginMutation, {
+ email: 'user@example.org',
+ password: 'supersecret',
+ }),
+ ).resolves.toEqual(expected)
+ })
+ })
+
+ describe('but expired code', () => {
+ beforeEach(async () => {
+ const issuedAt = new Date()
+ issuedAt.setDate(issuedAt.getDate() - 1)
+ await setup({ issuedAt })
+ })
+
+ it('resolves to false', async () => {
+ await expect(client.request(mutation, variables)).resolves.toEqual({
+ resetPassword: false,
+ })
+ })
+
+ it('does not update PasswordReset `usedAt` property', async () => {
+ await client.request(mutation, variables)
+ const requests = await getAllPasswordResets()
+ const [request] = requests
+ const { usedAt } = request.properties
+ expect(usedAt).toBeUndefined()
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/passwordReset/emailTemplates.js b/Human-Connection/backend/src/schema/resolvers/passwordReset/emailTemplates.js
new file mode 100644
index 000000000..8508adccc
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/passwordReset/emailTemplates.js
@@ -0,0 +1,85 @@
+import CONFIG from '../../../config'
+
+export const from = '"Human Connection" '
+
+export const resetPasswordMail = options => {
+ const {
+ name,
+ email,
+ code,
+ subject = 'Use this link to reset your password. The link is only valid for 24 hours.',
+ supportUrl = 'https://human-connection.org/en/contact/',
+ } = options
+ const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
+ actionUrl.searchParams.set('code', code)
+ actionUrl.searchParams.set('email', email)
+
+ return {
+ to: email,
+ subject,
+ text: `
+Hi ${name}!
+
+You recently requested to reset your password for your Human Connection account.
+Use the link below to reset it. This password reset is only valid for the next
+24 hours.
+
+${actionUrl}
+
+If you did not request a password reset, please ignore this email or contact
+support if you have questions:
+
+${supportUrl}
+
+Thanks,
+The Human Connection Team
+
+If you're having trouble with the link above, you can manually copy and
+paste the following code into your browser window:
+
+${code}
+
+Human Connection gemeinnützige GmbH
+Bahnhofstr. 11
+73235 Weilheim / Teck
+Deutschland
+ `,
+ }
+}
+
+export const wrongAccountMail = options => {
+ const {
+ email,
+ subject = `We received a request to reset your password with this email address (${email})`,
+ supportUrl = 'https://human-connection.org/en/contact/',
+ } = options
+ const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
+ return {
+ to: email,
+ subject,
+ text: `
+We received a request to reset the password to access Human Connection with your
+email address, but we were unable to find an account associated with this
+address.
+
+If you use Human Connection and were expecting this email, consider trying to
+request a password reset using the email address associated with your account.
+Try a different email:
+
+${actionUrl}
+
+If you do not use Human Connection or did not request a password reset, please
+ignore this email. Feel free to contact support if you have further questions:
+
+${supportUrl}
+
+Thanks,
+The Human Connection Team
+
+Human Connection gemeinnützige GmbH
+Bahnhofstr. 11
+73235 Weilheim / Teck
+Deutschland
+ `,
+ }
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/posts.js b/Human-Connection/backend/src/schema/resolvers/posts.js
new file mode 100644
index 000000000..0c8dfb7f0
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/posts.js
@@ -0,0 +1,74 @@
+import uuid from 'uuid/v4'
+import fileUpload from './fileUpload'
+
+export default {
+ Mutation: {
+ UpdatePost: async (object, params, context, resolveInfo) => {
+ const { categoryIds } = params
+ delete params.categoryIds
+ params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
+ const session = context.driver.session()
+ const cypherDeletePreviousRelations = `
+ MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
+ DELETE previousRelations
+ RETURN post, category
+ `
+
+ await session.run(cypherDeletePreviousRelations, { params })
+
+ let updatePostCypher = `MATCH (post:Post {id: $params.id})
+ SET post = $params
+ `
+ if (categoryIds && categoryIds.length) {
+ updatePostCypher += `WITH post
+ UNWIND $categoryIds AS categoryId
+ MATCH (category:Category {id: categoryId})
+ MERGE (post)-[:CATEGORIZED]->(category)
+ `
+ }
+ updatePostCypher += `RETURN post`
+ const updatePostVariables = { categoryIds, params }
+
+ const transactionRes = await session.run(updatePostCypher, updatePostVariables)
+ const [post] = transactionRes.records.map(record => {
+ return record.get('post')
+ })
+
+ session.close()
+
+ return post.properties
+ },
+
+ CreatePost: async (object, params, context, resolveInfo) => {
+ const { categoryIds } = params
+ delete params.categoryIds
+ params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
+ params.id = params.id || uuid()
+ let createPostCypher = `CREATE (post:Post {params})
+ WITH post
+ MATCH (author:User {id: $userId})
+ MERGE (post)<-[:WROTE]-(author)
+ `
+ if (categoryIds) {
+ createPostCypher += `WITH post
+ UNWIND $categoryIds AS categoryId
+ MATCH (category:Category {id: categoryId})
+ MERGE (post)-[:CATEGORIZED]->(category)
+ `
+ }
+ createPostCypher += `RETURN post`
+ const createPostVariables = { userId: context.user.id, categoryIds, params }
+
+ const session = context.driver.session()
+ const transactionRes = await session.run(createPostCypher, createPostVariables)
+
+ const [post] = transactionRes.records.map(record => {
+ return record.get('post')
+ })
+
+ session.close()
+
+ return post.properties
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/posts.spec.js b/Human-Connection/backend/src/schema/resolvers/posts.spec.js
new file mode 100644
index 000000000..763945527
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/posts.spec.js
@@ -0,0 +1,345 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../seed/factories'
+import { host, login } from '../../jest/helpers'
+
+const factory = Factory()
+let client
+const postTitle = 'I am a title'
+const postContent = 'Some content'
+const oldTitle = 'Old title'
+const oldContent = 'Old content'
+const newTitle = 'New title'
+const newContent = 'New content'
+const createPostVariables = { title: postTitle, content: postContent }
+const createPostWithCategoriesMutation = `
+ mutation($title: String!, $content: String!, $categoryIds: [ID]) {
+ CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
+ id
+ }
+ }
+`
+const creatPostWithCategoriesVariables = {
+ title: postTitle,
+ content: postContent,
+ categoryIds: ['cat9', 'cat4', 'cat15'],
+}
+const postQueryWithCategories = `
+ query($id: ID) {
+ Post(id: $id) {
+ categories {
+ id
+ }
+ }
+ }
+`
+beforeEach(async () => {
+ await factory.create('User', {
+ email: 'test@example.org',
+ password: '1234',
+ })
+})
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('CreatePost', () => {
+ const mutation = `
+ mutation($title: String!, $content: String!) {
+ CreatePost(title: $title, content: $content) {
+ title
+ content
+ slug
+ disabled
+ deleted
+ }
+ }
+ `
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(mutation, createPostVariables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated', () => {
+ let headers
+ beforeEach(async () => {
+ headers = await login({ email: 'test@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('creates a post', async () => {
+ const expected = {
+ CreatePost: {
+ title: postTitle,
+ content: postContent,
+ },
+ }
+ await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
+ })
+
+ it('assigns the authenticated user as author', async () => {
+ await client.request(mutation, createPostVariables)
+ const { User } = await client.request(
+ `{
+ User(email:"test@example.org") {
+ contributions {
+ title
+ }
+ }
+ }`,
+ { headers },
+ )
+ expect(User).toEqual([{ contributions: [{ title: postTitle }] }])
+ })
+
+ describe('disabled and deleted', () => {
+ it('initially false', async () => {
+ const expected = { CreatePost: { disabled: false, deleted: false } }
+ await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
+ })
+ })
+
+ describe('language', () => {
+ it('allows a user to set the language of the post', async () => {
+ const createPostWithLanguageMutation = `
+ mutation($title: String!, $content: String!, $language: String) {
+ CreatePost(title: $title, content: $content, language: $language) {
+ language
+ }
+ }
+ `
+ const createPostWithLanguageVariables = {
+ title: postTitle,
+ content: postContent,
+ language: 'en',
+ }
+ const expected = { CreatePost: { language: 'en' } }
+ await expect(
+ client.request(createPostWithLanguageMutation, createPostWithLanguageVariables),
+ ).resolves.toEqual(expect.objectContaining(expected))
+ })
+ })
+
+ describe('categories', () => {
+ it('allows a user to set the categories of the post', async () => {
+ await Promise.all([
+ factory.create('Category', {
+ id: 'cat9',
+ name: 'Democracy & Politics',
+ icon: 'university',
+ }),
+ factory.create('Category', {
+ id: 'cat4',
+ name: 'Environment & Nature',
+ icon: 'tree',
+ }),
+ factory.create('Category', {
+ id: 'cat15',
+ name: 'Consumption & Sustainability',
+ icon: 'shopping-cart',
+ }),
+ ])
+ const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]
+ const postWithCategories = await client.request(
+ createPostWithCategoriesMutation,
+ creatPostWithCategoriesVariables,
+ )
+ const postQueryWithCategoriesVariables = {
+ id: postWithCategories.CreatePost.id,
+ }
+ await expect(
+ client.request(postQueryWithCategories, postQueryWithCategoriesVariables),
+ ).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] })
+ })
+ })
+ })
+})
+
+describe('UpdatePost', () => {
+ let updatePostMutation
+ let updatePostVariables
+ beforeEach(async () => {
+ const asAuthor = Factory()
+ await asAuthor.create('User', {
+ email: 'author@example.org',
+ password: '1234',
+ })
+ await asAuthor.authenticateAs({
+ email: 'author@example.org',
+ password: '1234',
+ })
+ await asAuthor.create('Post', {
+ id: 'p1',
+ title: oldTitle,
+ content: oldContent,
+ })
+ updatePostMutation = `
+ mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
+ UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
+ id
+ content
+ }
+ }
+ `
+
+ updatePostVariables = {
+ id: 'p1',
+ title: newTitle,
+ content: newContent,
+ categoryIds: null,
+ }
+ })
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow(
+ 'Not Authorised',
+ )
+ })
+ })
+
+ describe('authenticated but not the author', () => {
+ let headers
+ beforeEach(async () => {
+ headers = await login({ email: 'test@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('throws authorization error', async () => {
+ await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow(
+ 'Not Authorised',
+ )
+ })
+ })
+
+ describe('authenticated as author', () => {
+ let headers
+ beforeEach(async () => {
+ headers = await login({ email: 'author@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('updates a post', async () => {
+ const expected = { UpdatePost: { id: 'p1', content: newContent } }
+ await expect(client.request(updatePostMutation, updatePostVariables)).resolves.toEqual(
+ expected,
+ )
+ })
+
+ describe('categories', () => {
+ let postWithCategories
+ beforeEach(async () => {
+ await Promise.all([
+ factory.create('Category', {
+ id: 'cat9',
+ name: 'Democracy & Politics',
+ icon: 'university',
+ }),
+ factory.create('Category', {
+ id: 'cat4',
+ name: 'Environment & Nature',
+ icon: 'tree',
+ }),
+ factory.create('Category', {
+ id: 'cat15',
+ name: 'Consumption & Sustainability',
+ icon: 'shopping-cart',
+ }),
+ factory.create('Category', {
+ id: 'cat27',
+ name: 'Animal Protection',
+ icon: 'paw',
+ }),
+ ])
+ postWithCategories = await client.request(
+ createPostWithCategoriesMutation,
+ creatPostWithCategoriesVariables,
+ )
+ updatePostVariables = {
+ id: postWithCategories.CreatePost.id,
+ title: newTitle,
+ content: newContent,
+ categoryIds: ['cat27'],
+ }
+ })
+
+ it('allows a user to update the categories of a post', async () => {
+ await client.request(updatePostMutation, updatePostVariables)
+ const expected = [{ id: 'cat27' }]
+ const postQueryWithCategoriesVariables = {
+ id: postWithCategories.CreatePost.id,
+ }
+ await expect(
+ client.request(postQueryWithCategories, postQueryWithCategoriesVariables),
+ ).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] })
+ })
+ })
+ })
+})
+
+describe('DeletePost', () => {
+ const mutation = `
+ mutation($id: ID!) {
+ DeletePost(id: $id) {
+ id
+ content
+ }
+ }
+ `
+
+ let variables = {
+ id: 'p1',
+ }
+
+ beforeEach(async () => {
+ const asAuthor = Factory()
+ await asAuthor.create('User', {
+ email: 'author@example.org',
+ password: '1234',
+ })
+ await asAuthor.authenticateAs({
+ email: 'author@example.org',
+ password: '1234',
+ })
+ await asAuthor.create('Post', {
+ id: 'p1',
+ content: 'To be deleted',
+ })
+ })
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated but not the author', () => {
+ let headers
+ beforeEach(async () => {
+ headers = await login({ email: 'test@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('throws authorization error', async () => {
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated as author', () => {
+ let headers
+ beforeEach(async () => {
+ headers = await login({ email: 'author@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('deletes a post', async () => {
+ const expected = { DeletePost: { id: 'p1', content: 'To be deleted' } }
+ await expect(client.request(mutation, variables)).resolves.toEqual(expected)
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/reports.js b/Human-Connection/backend/src/schema/resolvers/reports.js
new file mode 100644
index 000000000..67c896939
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/reports.js
@@ -0,0 +1,86 @@
+import uuid from 'uuid/v4'
+
+export default {
+ Mutation: {
+ report: async (parent, { id, description }, { driver, req, user }, resolveInfo) => {
+ const reportId = uuid()
+ const session = driver.session()
+ const reportData = {
+ id: reportId,
+ createdAt: new Date().toISOString(),
+ description: description,
+ }
+
+ const reportQueryRes = await session.run(
+ `
+ match (u:User {id:$submitterId}) -[:REPORTED]->(report)-[:REPORTED]-> (resource {id: $resourceId})
+ return labels(resource)[0] as label
+ `,
+ {
+ resourceId: id,
+ submitterId: user.id,
+ },
+ )
+ const [rep] = reportQueryRes.records.map(record => {
+ return {
+ label: record.get('label'),
+ }
+ })
+
+ if (rep) {
+ throw new Error(rep.label)
+ }
+ const res = await session.run(
+ `
+ MATCH (submitter:User {id: $userId})
+ MATCH (resource {id: $resourceId})
+ WHERE resource:User OR resource:Comment OR resource:Post
+ MERGE (report:Report {id: {reportData}.id })
+ MERGE (resource)<-[:REPORTED]-(report)
+ MERGE (report)<-[:REPORTED]-(submitter)
+ RETURN report, submitter, resource, labels(resource)[0] as type
+ `,
+ {
+ resourceId: id,
+ userId: user.id,
+ reportData,
+ },
+ )
+
+ session.close()
+
+ const [dbResponse] = res.records.map(r => {
+ return {
+ report: r.get('report'),
+ submitter: r.get('submitter'),
+ resource: r.get('resource'),
+ type: r.get('type'),
+ }
+ })
+ if (!dbResponse) return null
+ const { report, submitter, resource, type } = dbResponse
+
+ let response = {
+ ...report.properties,
+ post: null,
+ comment: null,
+ user: null,
+ submitter: submitter.properties,
+ type,
+ }
+ switch (type) {
+ case 'Post':
+ response.post = resource.properties
+ break
+ case 'Comment':
+ response.comment = resource.properties
+ break
+ case 'User':
+ response.user = resource.properties
+ break
+ }
+
+ return response
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/reports.spec.js b/Human-Connection/backend/src/schema/resolvers/reports.spec.js
new file mode 100644
index 000000000..2a798f5ee
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/reports.spec.js
@@ -0,0 +1,236 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../seed/factories'
+import { host, login } from '../../jest/helpers'
+
+const factory = Factory()
+
+describe('report', () => {
+ let mutation
+ let headers
+ let returnedObject
+ let variables
+ let createPostVariables
+
+ beforeEach(async () => {
+ returnedObject = '{ description }'
+ variables = {
+ id: 'whatever',
+ }
+ headers = {}
+ await factory.create('User', {
+ id: 'u1',
+ email: 'test@example.org',
+ password: '1234',
+ })
+ await factory.create('User', {
+ id: 'u2',
+ name: 'abusive-user',
+ role: 'user',
+ email: 'abusive-user@example.org',
+ })
+ })
+
+ afterEach(async () => {
+ await factory.cleanDatabase()
+ })
+
+ let client
+ const action = () => {
+ mutation = `
+ mutation($id: ID!) {
+ report(
+ id: $id,
+ description: "Violates code of conduct"
+ ) ${returnedObject}
+ }
+ `
+ client = new GraphQLClient(host, {
+ headers,
+ })
+ return client.request(mutation, variables)
+ }
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ await expect(action()).rejects.toThrow('Not Authorised')
+ })
+
+ describe('authenticated', () => {
+ beforeEach(async () => {
+ headers = await login({
+ email: 'test@example.org',
+ password: '1234',
+ })
+ })
+
+ describe('invalid resource id', () => {
+ it('returns null', async () => {
+ await expect(action()).resolves.toEqual({
+ report: null,
+ })
+ })
+ })
+
+ describe('valid resource id', () => {
+ beforeEach(async () => {
+ variables = {
+ id: 'u2',
+ }
+ })
+ /*
+ it('creates a report', async () => {
+ await expect(action()).resolves.toEqual({
+ type: null,
+ })
+ })
+ */
+ it('returns the submitter', async () => {
+ returnedObject = '{ submitter { email } }'
+ await expect(action()).resolves.toEqual({
+ report: {
+ submitter: {
+ email: 'test@example.org',
+ },
+ },
+ })
+ })
+
+ describe('reported resource is a user', () => {
+ it('returns type "User"', async () => {
+ returnedObject = '{ type }'
+ await expect(action()).resolves.toEqual({
+ report: {
+ type: 'User',
+ },
+ })
+ })
+
+ it('returns resource in user attribute', async () => {
+ returnedObject = '{ user { name } }'
+ await expect(action()).resolves.toEqual({
+ report: {
+ user: {
+ name: 'abusive-user',
+ },
+ },
+ })
+ })
+ })
+
+ describe('reported resource is a post', () => {
+ beforeEach(async () => {
+ await factory.authenticateAs({
+ email: 'test@example.org',
+ password: '1234',
+ })
+ await factory.create('Post', {
+ id: 'p23',
+ title: 'Matt and Robert having a pair-programming',
+ })
+ variables = {
+ id: 'p23',
+ }
+ })
+
+ it('returns type "Post"', async () => {
+ returnedObject = '{ type }'
+ await expect(action()).resolves.toEqual({
+ report: {
+ type: 'Post',
+ },
+ })
+ })
+
+ it('returns resource in post attribute', async () => {
+ returnedObject = '{ post { title } }'
+ await expect(action()).resolves.toEqual({
+ report: {
+ post: {
+ title: 'Matt and Robert having a pair-programming',
+ },
+ },
+ })
+ })
+
+ it('returns null in user attribute', async () => {
+ returnedObject = '{ user { name } }'
+ await expect(action()).resolves.toEqual({
+ report: {
+ user: null,
+ },
+ })
+ })
+ })
+
+ /* An der Stelle würde ich den p23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
+ At this point I would check the p23 again, but this time there must be an error message. */
+
+ describe('reported resource is a comment', () => {
+ beforeEach(async () => {
+ createPostVariables = {
+ id: 'p1',
+ title: 'post to comment on',
+ content: 'please comment on me',
+ }
+ const asAuthenticatedUser = await factory.authenticateAs({
+ email: 'test@example.org',
+ password: '1234',
+ })
+ await asAuthenticatedUser.create('Post', createPostVariables)
+ await asAuthenticatedUser.create('Comment', {
+ postId: 'p1',
+ id: 'c34',
+ content: 'Robert getting tired.',
+ })
+ variables = {
+ id: 'c34',
+ }
+ })
+
+ it('returns type "Comment"', async () => {
+ returnedObject = '{ type }'
+ await expect(action()).resolves.toEqual({
+ report: {
+ type: 'Comment',
+ },
+ })
+ })
+
+ it('returns resource in comment attribute', async () => {
+ returnedObject = '{ comment { content } }'
+ await expect(action()).resolves.toEqual({
+ report: {
+ comment: {
+ content: 'Robert getting tired.',
+ },
+ },
+ })
+ })
+ })
+
+ /* An der Stelle würde ich den c34 noch mal prüfen, diesmal muss aber eine error meldung kommen.
+ At this point I would check the c34 again, but this time there must be an error message. */
+
+ describe('reported resource is a tag', () => {
+ beforeEach(async () => {
+ await factory.create('Tag', {
+ id: 't23',
+ })
+ variables = {
+ id: 't23',
+ }
+ })
+
+ it('returns null', async () => {
+ await expect(action()).resolves.toEqual({
+ report: null,
+ })
+ })
+ })
+
+ /* An der Stelle würde ich den t23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
+ At this point I would check the t23 again, but this time there must be an error message. */
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/rewards.js b/Human-Connection/backend/src/schema/resolvers/rewards.js
new file mode 100644
index 000000000..ec5043da3
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/rewards.js
@@ -0,0 +1,47 @@
+export default {
+ Mutation: {
+ reward: async (_object, params, context, _resolveInfo) => {
+ const { fromBadgeId, toUserId } = params
+ const session = context.driver.session()
+
+ let transactionRes = await session.run(
+ `MATCH (badge:Badge {id: $badgeId}), (rewardedUser:User {id: $rewardedUserId})
+ MERGE (badge)-[:REWARDED]->(rewardedUser)
+ RETURN rewardedUser {.id}`,
+ {
+ badgeId: fromBadgeId,
+ rewardedUserId: toUserId,
+ },
+ )
+
+ const [rewardedUser] = transactionRes.records.map(record => {
+ return record.get('rewardedUser')
+ })
+
+ session.close()
+
+ return rewardedUser.id
+ },
+
+ unreward: async (_object, params, context, _resolveInfo) => {
+ const { fromBadgeId, toUserId } = params
+ const session = context.driver.session()
+
+ let transactionRes = await session.run(
+ `MATCH (badge:Badge {id: $badgeId})-[reward:REWARDED]->(rewardedUser:User {id: $rewardedUserId})
+ DELETE reward
+ RETURN rewardedUser {.id}`,
+ {
+ badgeId: fromBadgeId,
+ rewardedUserId: toUserId,
+ },
+ )
+ const [rewardedUser] = transactionRes.records.map(record => {
+ return record.get('rewardedUser')
+ })
+ session.close()
+
+ return rewardedUser.id
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/rewards.spec.js b/Human-Connection/backend/src/schema/resolvers/rewards.spec.js
new file mode 100644
index 000000000..2bdd9a39b
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/rewards.spec.js
@@ -0,0 +1,214 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../seed/factories'
+import { host, login } from '../../jest/helpers'
+
+const factory = Factory()
+
+describe('rewards', () => {
+ beforeEach(async () => {
+ await factory.create('User', {
+ id: 'u1',
+ role: 'user',
+ email: 'user@example.org',
+ password: '1234',
+ })
+ await factory.create('User', {
+ id: 'u2',
+ role: 'moderator',
+ email: 'moderator@example.org',
+ })
+ await factory.create('User', {
+ id: 'u3',
+ role: 'admin',
+ email: 'admin@example.org',
+ })
+ await factory.create('Badge', {
+ id: 'b6',
+ key: 'indiegogo_en_rhino',
+ type: 'crowdfunding',
+ status: 'permanent',
+ icon: '/img/badges/indiegogo_en_rhino.svg',
+ })
+ })
+
+ afterEach(async () => {
+ await factory.cleanDatabase()
+ })
+
+ describe('RewardBadge', () => {
+ const mutation = `
+ mutation(
+ $from: ID!
+ $to: ID!
+ ) {
+ reward(fromBadgeId: $from, toUserId: $to)
+ }
+ `
+
+ describe('unauthenticated', () => {
+ const variables = {
+ from: 'b6',
+ to: 'u1',
+ }
+ let client
+
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated admin', () => {
+ let client
+ beforeEach(async () => {
+ const headers = await login({ email: 'admin@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('rewards a badge to user', async () => {
+ const variables = {
+ from: 'b6',
+ to: 'u1',
+ }
+ const expected = {
+ reward: 'u1',
+ }
+ await expect(client.request(mutation, variables)).resolves.toEqual(expected)
+ })
+ it('rewards a second different badge to same user', async () => {
+ await factory.create('Badge', {
+ id: 'b1',
+ key: 'indiegogo_en_racoon',
+ type: 'crowdfunding',
+ status: 'permanent',
+ icon: '/img/badges/indiegogo_en_racoon.svg',
+ })
+ const variables = {
+ from: 'b1',
+ to: 'u1',
+ }
+ const expected = {
+ reward: 'u1',
+ }
+ await expect(client.request(mutation, variables)).resolves.toEqual(expected)
+ })
+ it('rewards the same badge as well to another user', async () => {
+ const variables1 = {
+ from: 'b6',
+ to: 'u1',
+ }
+ await client.request(mutation, variables1)
+
+ const variables2 = {
+ from: 'b6',
+ to: 'u2',
+ }
+ const expected = {
+ reward: 'u2',
+ }
+ await expect(client.request(mutation, variables2)).resolves.toEqual(expected)
+ })
+ it('returns the original reward if a reward is attempted a second time', async () => {
+ const variables = {
+ from: 'b6',
+ to: 'u1',
+ }
+ await client.request(mutation, variables)
+ await client.request(mutation, variables)
+
+ const query = `{
+ User( id: "u1" ) {
+ badgesCount
+ }
+ }
+ `
+ const expected = { User: [{ badgesCount: 1 }] }
+
+ await expect(client.request(query)).resolves.toEqual(expected)
+ })
+ })
+
+ describe('authenticated moderator', () => {
+ const variables = {
+ from: 'b6',
+ to: 'u1',
+ }
+ let client
+ beforeEach(async () => {
+ const headers = await login({ email: 'moderator@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ describe('rewards bage to user', () => {
+ it('throws authorization error', async () => {
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+ })
+ })
+
+ describe('RemoveReward', () => {
+ beforeEach(async () => {
+ await factory.relate('User', 'Badges', { from: 'b6', to: 'u1' })
+ })
+ const variables = {
+ from: 'b6',
+ to: 'u1',
+ }
+ const expected = {
+ unreward: 'u1',
+ }
+
+ const mutation = `
+ mutation(
+ $from: ID!
+ $to: ID!
+ ) {
+ unreward(fromBadgeId: $from, toUserId: $to)
+ }
+ `
+
+ describe('unauthenticated', () => {
+ let client
+
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated admin', () => {
+ let client
+ beforeEach(async () => {
+ const headers = await login({ email: 'admin@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('removes a badge from user', async () => {
+ await expect(client.request(mutation, variables)).resolves.toEqual(expected)
+ })
+
+ it('fails to remove a not existing badge from user', async () => {
+ await client.request(mutation, variables)
+
+ await expect(client.request(mutation, variables)).rejects.toThrow(
+ "Cannot read property 'id' of undefined",
+ )
+ })
+ })
+
+ describe('authenticated moderator', () => {
+ let client
+ beforeEach(async () => {
+ const headers = await login({ email: 'moderator@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ describe('removes bage from user', () => {
+ it('throws authorization error', async () => {
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/shout.js b/Human-Connection/backend/src/schema/resolvers/shout.js
new file mode 100644
index 000000000..d2d7f652e
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/shout.js
@@ -0,0 +1,51 @@
+export default {
+ Mutation: {
+ shout: async (_object, params, context, _resolveInfo) => {
+ const { id, type } = params
+
+ const session = context.driver.session()
+ let transactionRes = await session.run(
+ `MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId})
+ WHERE $type IN labels(node) AND NOT userWritten.id = $userId
+ MERGE (user)-[relation:SHOUTED]->(node)
+ RETURN COUNT(relation) > 0 as isShouted`,
+ {
+ id,
+ type,
+ userId: context.user.id,
+ },
+ )
+
+ const [isShouted] = transactionRes.records.map(record => {
+ return record.get('isShouted')
+ })
+
+ session.close()
+
+ return isShouted
+ },
+
+ unshout: async (_object, params, context, _resolveInfo) => {
+ const { id, type } = params
+ const session = context.driver.session()
+
+ let transactionRes = await session.run(
+ `MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id})
+ WHERE $type IN labels(node)
+ DELETE relation
+ RETURN COUNT(relation) > 0 as isShouted`,
+ {
+ id,
+ type,
+ userId: context.user.id,
+ },
+ )
+ const [isShouted] = transactionRes.records.map(record => {
+ return record.get('isShouted')
+ })
+ session.close()
+
+ return isShouted
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/shout.spec.js b/Human-Connection/backend/src/schema/resolvers/shout.spec.js
new file mode 100644
index 000000000..a94f7ca0b
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/shout.spec.js
@@ -0,0 +1,139 @@
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../seed/factories'
+import { host, login } from '../../jest/helpers'
+
+const factory = Factory()
+let clientUser1, clientUser2
+let headersUser1, headersUser2
+
+const mutationShoutPost = id => `
+ mutation {
+ shout(id: "${id}", type: Post)
+ }
+`
+const mutationUnshoutPost = id => `
+ mutation {
+ unshout(id: "${id}", type: Post)
+ }
+`
+
+beforeEach(async () => {
+ await factory.create('User', {
+ id: 'u1',
+ email: 'test@example.org',
+ password: '1234',
+ })
+ await factory.create('User', {
+ id: 'u2',
+ email: 'test2@example.org',
+ password: '1234',
+ })
+
+ headersUser1 = await login({ email: 'test@example.org', password: '1234' })
+ headersUser2 = await login({ email: 'test2@example.org', password: '1234' })
+ clientUser1 = new GraphQLClient(host, { headers: headersUser1 })
+ clientUser2 = new GraphQLClient(host, { headers: headersUser2 })
+
+ await clientUser1.request(`
+ mutation {
+ CreatePost(id: "p1", title: "Post Title 1", content: "Some Post Content 1") {
+ id
+ title
+ }
+ }
+ `)
+ await clientUser2.request(`
+ mutation {
+ CreatePost(id: "p2", title: "Post Title 2", content: "Some Post Content 2") {
+ id
+ title
+ }
+ }
+ `)
+})
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('shout', () => {
+ describe('shout foreign post', () => {
+ describe('unauthenticated shout', () => {
+ it('throws authorization error', async () => {
+ let client
+ client = new GraphQLClient(host)
+ await expect(client.request(mutationShoutPost('p1'))).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ it('I shout a post of another user', async () => {
+ const res = await clientUser1.request(mutationShoutPost('p2'))
+ const expected = {
+ shout: true,
+ }
+ expect(res).toMatchObject(expected)
+
+ const { Post } = await clientUser1.request(`{
+ Post(id: "p2") {
+ shoutedByCurrentUser
+ }
+ }`)
+ const expected2 = {
+ shoutedByCurrentUser: true,
+ }
+ expect(Post[0]).toMatchObject(expected2)
+ })
+
+ it('I can`t shout my own post', async () => {
+ const res = await clientUser1.request(mutationShoutPost('p1'))
+ const expected = {
+ shout: false,
+ }
+ expect(res).toMatchObject(expected)
+
+ const { Post } = await clientUser1.request(`{
+ Post(id: "p1") {
+ shoutedByCurrentUser
+ }
+ }`)
+ const expected2 = {
+ shoutedByCurrentUser: false,
+ }
+ expect(Post[0]).toMatchObject(expected2)
+ })
+ })
+
+ describe('unshout foreign post', () => {
+ describe('unauthenticated shout', () => {
+ it('throws authorization error', async () => {
+ // shout
+ await clientUser1.request(mutationShoutPost('p2'))
+ // unshout
+ let client
+ client = new GraphQLClient(host)
+ await expect(client.request(mutationUnshoutPost('p2'))).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ it('I unshout a post of another user', async () => {
+ // shout
+ await clientUser1.request(mutationShoutPost('p2'))
+ const expected = {
+ unshout: true,
+ }
+ // unshout
+ const res = await clientUser1.request(mutationUnshoutPost('p2'))
+ expect(res).toMatchObject(expected)
+
+ const { Post } = await clientUser1.request(`{
+ Post(id: "p2") {
+ shoutedByCurrentUser
+ }
+ }`)
+ const expected2 = {
+ shoutedByCurrentUser: false,
+ }
+ expect(Post[0]).toMatchObject(expected2)
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/socialMedia.js b/Human-Connection/backend/src/schema/resolvers/socialMedia.js
new file mode 100644
index 000000000..0bc03ea74
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/socialMedia.js
@@ -0,0 +1,30 @@
+import { neo4jgraphql } from 'neo4j-graphql-js'
+
+export default {
+ Mutation: {
+ CreateSocialMedia: async (object, params, context, resolveInfo) => {
+ /**
+ * TODO?: Creates double Nodes!
+ */
+ const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
+ const session = context.driver.session()
+ await session.run(
+ `MATCH (owner:User {id: $userId}), (socialMedia:SocialMedia {id: $socialMediaId})
+ MERGE (socialMedia)<-[:OWNED]-(owner)
+ RETURN owner`,
+ {
+ userId: context.user.id,
+ socialMediaId: socialMedia.id,
+ },
+ )
+ session.close()
+
+ return socialMedia
+ },
+ DeleteSocialMedia: async (object, params, context, resolveInfo) => {
+ const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
+
+ return socialMedia
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/socialMedia.spec.js b/Human-Connection/backend/src/schema/resolvers/socialMedia.spec.js
new file mode 100644
index 000000000..38850761c
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/socialMedia.spec.js
@@ -0,0 +1,111 @@
+import gql from 'graphql-tag'
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../seed/factories'
+import { host, login } from '../../jest/helpers'
+
+const factory = Factory()
+
+describe('SocialMedia', () => {
+ let client
+ let headers
+ const mutationC = gql`
+ mutation($url: String!) {
+ CreateSocialMedia(url: $url) {
+ id
+ url
+ }
+ }
+ `
+ const mutationD = gql`
+ mutation($id: ID!) {
+ DeleteSocialMedia(id: $id) {
+ id
+ url
+ }
+ }
+ `
+ beforeEach(async () => {
+ await factory.create('User', {
+ avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
+ id: 'acb2d923-f3af-479e-9f00-61b12e864666',
+ name: 'Matilde Hermiston',
+ slug: 'matilde-hermiston',
+ role: 'user',
+ email: 'test@example.org',
+ password: '1234',
+ })
+ })
+
+ afterEach(async () => {
+ await factory.cleanDatabase()
+ })
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ const variables = {
+ url: 'http://nsosp.org',
+ }
+ await expect(client.request(mutationC, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated', () => {
+ beforeEach(async () => {
+ headers = await login({
+ email: 'test@example.org',
+ password: '1234',
+ })
+ client = new GraphQLClient(host, {
+ headers,
+ })
+ })
+
+ it('creates social media with correct URL', async () => {
+ const variables = {
+ url: 'http://nsosp.org',
+ }
+ await expect(client.request(mutationC, variables)).resolves.toEqual(
+ expect.objectContaining({
+ CreateSocialMedia: {
+ id: expect.any(String),
+ url: 'http://nsosp.org',
+ },
+ }),
+ )
+ })
+
+ it('deletes social media', async () => {
+ const creationVariables = {
+ url: 'http://nsosp.org',
+ }
+ const { CreateSocialMedia } = await client.request(mutationC, creationVariables)
+ const { id } = CreateSocialMedia
+
+ const deletionVariables = {
+ id,
+ }
+ const expected = {
+ DeleteSocialMedia: {
+ id: id,
+ url: 'http://nsosp.org',
+ },
+ }
+ await expect(client.request(mutationD, deletionVariables)).resolves.toEqual(expected)
+ })
+
+ it('rejects empty string', async () => {
+ const variables = {
+ url: '',
+ }
+ await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL')
+ })
+
+ it('validates URLs', async () => {
+ const variables = {
+ url: 'not-a-url',
+ }
+ await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL')
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/statistics.js b/Human-Connection/backend/src/schema/resolvers/statistics.js
new file mode 100644
index 000000000..f09b7219d
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/statistics.js
@@ -0,0 +1,74 @@
+export const query = (cypher, session) => {
+ return new Promise((resolve, reject) => {
+ let data = []
+ session.run(cypher).subscribe({
+ onNext: function(record) {
+ let item = {}
+ record.keys.forEach(key => {
+ item[key] = record.get(key)
+ })
+ data.push(item)
+ },
+ onCompleted: function() {
+ session.close()
+ resolve(data)
+ },
+ onError: function(error) {
+ reject(error)
+ },
+ })
+ })
+}
+const queryOne = (cypher, session) => {
+ return new Promise((resolve, reject) => {
+ query(cypher, session)
+ .then(res => {
+ resolve(res.length ? res.pop() : {})
+ })
+ .catch(err => {
+ reject(err)
+ })
+ })
+}
+
+export default {
+ Query: {
+ statistics: async (parent, args, { driver, user }) => {
+ return new Promise(async resolve => {
+ const session = driver.session()
+ const queries = {
+ countUsers:
+ 'MATCH (r:User) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countUsers',
+ countPosts:
+ 'MATCH (r:Post) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countPosts',
+ countComments:
+ 'MATCH (r:Comment) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countComments',
+ countNotifications:
+ 'MATCH (r:Notification) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countNotifications',
+ countOrganizations:
+ 'MATCH (r:Organization) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countOrganizations',
+ countProjects:
+ 'MATCH (r:Project) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countProjects',
+ countInvites:
+ 'MATCH (r:Invite) WHERE r.wasUsed <> true OR NOT exists(r.wasUsed) RETURN COUNT(r) AS countInvites',
+ countFollows: 'MATCH (:User)-[r:FOLLOWS]->(:User) RETURN COUNT(r) AS countFollows',
+ countShouts: 'MATCH (:User)-[r:SHOUTED]->(:Post) RETURN COUNT(r) AS countShouts',
+ }
+ let data = {
+ countUsers: (await queryOne(queries.countUsers, session)).countUsers.low,
+ countPosts: (await queryOne(queries.countPosts, session)).countPosts.low,
+ countComments: (await queryOne(queries.countComments, session)).countComments.low,
+ countNotifications: (await queryOne(queries.countNotifications, session))
+ .countNotifications.low,
+ countOrganizations: (await queryOne(queries.countOrganizations, session))
+ .countOrganizations.low,
+ countProjects: (await queryOne(queries.countProjects, session)).countProjects.low,
+ countInvites: (await queryOne(queries.countInvites, session)).countInvites.low,
+ countFollows: (await queryOne(queries.countFollows, session)).countFollows.low,
+ countShouts: (await queryOne(queries.countShouts, session)).countShouts.low,
+ }
+ resolve(data)
+ })
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/user_management.js b/Human-Connection/backend/src/schema/resolvers/user_management.js
new file mode 100644
index 000000000..e33314f7e
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/user_management.js
@@ -0,0 +1,97 @@
+import encode from '../../jwt/encode'
+import bcrypt from 'bcryptjs'
+import { AuthenticationError } from 'apollo-server'
+import { neo4jgraphql } from 'neo4j-graphql-js'
+
+export default {
+ Query: {
+ isLoggedIn: (parent, args, { driver, user }) => {
+ return Boolean(user && user.id)
+ },
+ currentUser: async (object, params, ctx, resolveInfo) => {
+ const { user } = ctx
+ if (!user) return null
+ return neo4jgraphql(object, { id: user.id }, ctx, resolveInfo, false)
+ },
+ },
+ Mutation: {
+ signup: async (parent, { email, password }, { req }) => {
+ // if (data[email]) {
+ // throw new Error('Another User with same email exists.')
+ // }
+ // data[email] = {
+ // password: await bcrypt.hashSync(password, 10),
+ // }
+
+ return true
+ },
+ login: async (parent, { email, password }, { driver, req, user }) => {
+ // if (user && user.id) {
+ // throw new Error('Already logged in.')
+ // }
+ const session = driver.session()
+ const result = await session.run(
+ 'MATCH (user:User {email: $userEmail}) ' +
+ 'RETURN user {.id, .slug, .name, .avatar, .email, .password, .role, .disabled} as user LIMIT 1',
+ {
+ userEmail: email,
+ },
+ )
+
+ session.close()
+ const [currentUser] = await result.records.map(function(record) {
+ return record.get('user')
+ })
+
+ if (
+ currentUser &&
+ (await bcrypt.compareSync(password, currentUser.password)) &&
+ !currentUser.disabled
+ ) {
+ delete currentUser.password
+ return encode(currentUser)
+ } else if (currentUser && currentUser.disabled) {
+ throw new AuthenticationError('Your account has been disabled.')
+ } else {
+ throw new AuthenticationError('Incorrect email address or password.')
+ }
+ },
+ changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
+ const session = driver.session()
+ let result = await session.run(
+ `MATCH (user:User {email: $userEmail})
+ RETURN user {.id, .email, .password}`,
+ {
+ userEmail: user.email,
+ },
+ )
+
+ const [currentUser] = result.records.map(function(record) {
+ return record.get('user')
+ })
+
+ if (!(await bcrypt.compareSync(oldPassword, currentUser.password))) {
+ throw new AuthenticationError('Old password is not correct')
+ }
+
+ if (await bcrypt.compareSync(newPassword, currentUser.password)) {
+ throw new AuthenticationError('Old password and new password should be different')
+ } else {
+ const newHashedPassword = await bcrypt.hashSync(newPassword, 10)
+ session.run(
+ `MATCH (user:User {email: $userEmail})
+ SET user.password = $newHashedPassword
+ RETURN user
+ `,
+ {
+ userEmail: user.email,
+ newHashedPassword,
+ },
+ )
+ session.close()
+
+ return encode(currentUser)
+ }
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/user_management.spec.js b/Human-Connection/backend/src/schema/resolvers/user_management.spec.js
new file mode 100644
index 000000000..463c5ea6d
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/user_management.spec.js
@@ -0,0 +1,431 @@
+import gql from 'graphql-tag'
+import { GraphQLClient, request } from 'graphql-request'
+import jwt from 'jsonwebtoken'
+import CONFIG from './../../config'
+import Factory from '../../seed/factories'
+import { host, login } from '../../jest/helpers'
+
+const factory = Factory()
+
+// here is the decoded JWT token:
+// {
+// role: 'user',
+// locationName: null,
+// name: 'Jenny Rostock',
+// about: null,
+// avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
+// id: 'u3',
+// email: 'user@example.org',
+// slug: 'jenny-rostock',
+// iat: 1550846680,
+// exp: 1637246680,
+// aud: 'http://localhost:3000',
+// iss: 'http://localhost:4000',
+// sub: 'u3'
+// }
+const jennyRostocksHeaders = {
+ authorization:
+ 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc',
+}
+
+const disable = async id => {
+ const moderatorParams = { email: 'moderator@example.org', role: 'moderator', password: '1234' }
+ const asModerator = Factory()
+ await asModerator.create('User', moderatorParams)
+ await asModerator.authenticateAs(moderatorParams)
+ await asModerator.mutate('mutation($id: ID!) { disable(id: $id) }', { id })
+}
+
+beforeEach(async () => {
+ await factory.create('User', {
+ avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
+ id: 'acb2d923-f3af-479e-9f00-61b12e864666',
+ name: 'Matilde Hermiston',
+ slug: 'matilde-hermiston',
+ role: 'user',
+ email: 'test@example.org',
+ password: '1234',
+ })
+})
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('isLoggedIn', () => {
+ const query = '{ isLoggedIn }'
+ describe('unauthenticated', () => {
+ it('returns false', async () => {
+ await expect(request(host, query)).resolves.toEqual({
+ isLoggedIn: false,
+ })
+ })
+ })
+
+ describe('with malformed JWT Bearer token', () => {
+ const headers = { authorization: 'blah' }
+ const client = new GraphQLClient(host, { headers })
+
+ it('returns false', async () => {
+ await expect(client.request(query)).resolves.toEqual({
+ isLoggedIn: false,
+ })
+ })
+ })
+
+ describe('with valid JWT Bearer token', () => {
+ const client = new GraphQLClient(host, { headers: jennyRostocksHeaders })
+
+ it('returns false', async () => {
+ await expect(client.request(query)).resolves.toEqual({
+ isLoggedIn: false,
+ })
+ })
+
+ describe('and a corresponding user in the database', () => {
+ describe('user is enabled', () => {
+ it('returns true', async () => {
+ // see the decoded token above
+ await factory.create('User', { id: 'u3' })
+ await expect(client.request(query)).resolves.toEqual({
+ isLoggedIn: true,
+ })
+ })
+ })
+
+ describe('user is disabled', () => {
+ beforeEach(async () => {
+ await factory.create('User', { id: 'u3' })
+ await disable('u3')
+ })
+
+ it('returns false', async () => {
+ await expect(client.request(query)).resolves.toEqual({
+ isLoggedIn: false,
+ })
+ })
+ })
+ })
+ })
+})
+
+describe('currentUser', () => {
+ const query = `{
+ currentUser {
+ id
+ slug
+ name
+ avatar
+ email
+ role
+ }
+ }`
+
+ describe('unauthenticated', () => {
+ it('returns null', async () => {
+ const expected = { currentUser: null }
+ await expect(request(host, query)).resolves.toEqual(expected)
+ })
+ })
+
+ describe('with valid JWT Bearer Token', () => {
+ let client
+ let headers
+
+ describe('but no corresponding user in the database', () => {
+ beforeEach(async () => {
+ client = new GraphQLClient(host, { headers: jennyRostocksHeaders })
+ })
+
+ it('returns null', async () => {
+ const expected = { currentUser: null }
+ await expect(client.request(query)).resolves.toEqual(expected)
+ })
+ })
+
+ describe('and corresponding user in the database', () => {
+ beforeEach(async () => {
+ headers = await login({ email: 'test@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('returns the whole user object', async () => {
+ const expected = {
+ currentUser: {
+ avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
+ email: 'test@example.org',
+ id: 'acb2d923-f3af-479e-9f00-61b12e864666',
+ name: 'Matilde Hermiston',
+ slug: 'matilde-hermiston',
+ role: 'user',
+ },
+ }
+ await expect(client.request(query)).resolves.toEqual(expected)
+ })
+ })
+ })
+})
+
+describe('login', () => {
+ const mutation = params => {
+ const { email, password } = params
+ return `
+ mutation {
+ login(email:"${email}", password:"${password}")
+ }`
+ }
+
+ describe('ask for a `token`', () => {
+ describe('with valid email/password combination', () => {
+ it('responds with a JWT token', async () => {
+ const data = await request(
+ host,
+ mutation({
+ email: 'test@example.org',
+ password: '1234',
+ }),
+ )
+ const token = data.login
+ jwt.verify(token, CONFIG.JWT_SECRET, (err, data) => {
+ expect(data.email).toEqual('test@example.org')
+ expect(err).toBeNull()
+ })
+ })
+ })
+
+ describe('valid email/password but user is disabled', () => {
+ it('responds with "Your account has been disabled."', async () => {
+ await disable('acb2d923-f3af-479e-9f00-61b12e864666')
+ await expect(
+ request(
+ host,
+ mutation({
+ email: 'test@example.org',
+ password: '1234',
+ }),
+ ),
+ ).rejects.toThrow('Your account has been disabled.')
+ })
+ })
+
+ describe('with a valid email but incorrect password', () => {
+ it('responds with "Incorrect email address or password."', async () => {
+ await expect(
+ request(
+ host,
+ mutation({
+ email: 'test@example.org',
+ password: 'wrong',
+ }),
+ ),
+ ).rejects.toThrow('Incorrect email address or password.')
+ })
+ })
+
+ describe('with a non-existing email', () => {
+ it('responds with "Incorrect email address or password."', async () => {
+ await expect(
+ request(
+ host,
+ mutation({
+ email: 'non-existent@example.org',
+ password: 'wrong',
+ }),
+ ),
+ ).rejects.toThrow('Incorrect email address or password.')
+ })
+ })
+ })
+})
+
+describe('change password', () => {
+ let headers
+ let client
+
+ beforeEach(async () => {
+ headers = await login({ email: 'test@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ const mutation = params => {
+ const { oldPassword, newPassword } = params
+ return `
+ mutation {
+ changePassword(oldPassword:"${oldPassword}", newPassword:"${newPassword}")
+ }`
+ }
+
+ describe('should be authenticated before changing password', () => {
+ it('throws "Not Authorised!"', async () => {
+ await expect(
+ request(
+ host,
+ mutation({
+ oldPassword: '1234',
+ newPassword: '1234',
+ }),
+ ),
+ ).rejects.toThrow('Not Authorised!')
+ })
+ })
+
+ describe('old and new password should not match', () => {
+ it('responds with "Old password and new password should be different"', async () => {
+ await expect(
+ client.request(
+ mutation({
+ oldPassword: '1234',
+ newPassword: '1234',
+ }),
+ ),
+ ).rejects.toThrow('Old password and new password should be different')
+ })
+ })
+
+ describe('incorrect old password', () => {
+ it('responds with "Old password isn\'t valid"', async () => {
+ await expect(
+ client.request(
+ mutation({
+ oldPassword: 'notOldPassword',
+ newPassword: '12345',
+ }),
+ ),
+ ).rejects.toThrow('Old password is not correct')
+ })
+ })
+
+ describe('correct password', () => {
+ it('changes the password if given correct credentials "', async () => {
+ let response = await client.request(
+ mutation({
+ oldPassword: '1234',
+ newPassword: '12345',
+ }),
+ )
+ await expect(response).toEqual(
+ expect.objectContaining({
+ changePassword: expect.any(String),
+ }),
+ )
+ })
+ })
+})
+
+describe('do not expose private RSA key', () => {
+ let headers
+ let client
+ let authenticatedClient
+
+ const queryUserPuplicKey = gql`
+ query($queriedUserSlug: String) {
+ User(slug: $queriedUserSlug) {
+ id
+ publicKey
+ }
+ }
+ `
+ const queryUserPrivateKey = gql`
+ query($queriedUserSlug: String) {
+ User(slug: $queriedUserSlug) {
+ id
+ privateKey
+ }
+ }
+ `
+
+ const generateUserWithKeys = async authenticatedClient => {
+ // Generate user with "privateKey" via 'CreateUser' mutation instead of using the factories "factory.create('User', {...})", see above.
+ const variables = {
+ id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
+ password: 'xYz',
+ slug: 'apfel-strudel',
+ name: 'Apfel Strudel',
+ email: 'apfel-strudel@test.org',
+ }
+ await authenticatedClient.request(
+ gql`
+ mutation($id: ID, $password: String!, $slug: String, $name: String, $email: String!) {
+ CreateUser(id: $id, password: $password, slug: $slug, name: $name, email: $email) {
+ id
+ }
+ }
+ `,
+ variables,
+ )
+ }
+
+ beforeEach(async () => {
+ const adminParams = {
+ role: 'admin',
+ email: 'admin@example.org',
+ password: '1234',
+ }
+ // create an admin user who has enough permissions to create other users
+ await factory.create('User', adminParams)
+ const headers = await login(adminParams)
+ authenticatedClient = new GraphQLClient(host, { headers })
+ // but also create an unauthenticated client to issue the `User` query
+ client = new GraphQLClient(host)
+ })
+
+ describe('unauthenticated query of "publicKey" (does the RSA key pair get generated at all?)', () => {
+ it('returns publicKey', async () => {
+ await generateUserWithKeys(authenticatedClient)
+ await expect(
+ await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }),
+ ).toEqual(
+ expect.objectContaining({
+ User: [
+ {
+ id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
+ publicKey: expect.any(String),
+ },
+ ],
+ }),
+ )
+ })
+ })
+
+ describe('unauthenticated query of "privateKey"', () => {
+ it('throws "Not Authorised!"', async () => {
+ await generateUserWithKeys(authenticatedClient)
+ await expect(
+ client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }),
+ ).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ // authenticate
+ beforeEach(async () => {
+ headers = await login({ email: 'test@example.org', password: '1234' })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ describe('authenticated query of "publicKey"', () => {
+ it('returns publicKey', async () => {
+ await generateUserWithKeys(authenticatedClient)
+ await expect(
+ await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }),
+ ).toEqual(
+ expect.objectContaining({
+ User: [
+ {
+ id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
+ publicKey: expect.any(String),
+ },
+ ],
+ }),
+ )
+ })
+ })
+
+ describe('authenticated query of "privateKey"', () => {
+ it('throws "Not Authorised!"', async () => {
+ await generateUserWithKeys(authenticatedClient)
+ await expect(
+ client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }),
+ ).rejects.toThrow('Not Authorised')
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/resolvers/users.js b/Human-Connection/backend/src/schema/resolvers/users.js
new file mode 100644
index 000000000..c5c3701b5
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/users.js
@@ -0,0 +1,37 @@
+import { neo4jgraphql } from 'neo4j-graphql-js'
+import fileUpload from './fileUpload'
+
+export default {
+ Mutation: {
+ UpdateUser: async (object, params, context, resolveInfo) => {
+ params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
+ return neo4jgraphql(object, params, context, resolveInfo, false)
+ },
+ CreateUser: async (object, params, context, resolveInfo) => {
+ params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
+ return neo4jgraphql(object, params, context, resolveInfo, false)
+ },
+ DeleteUser: async (object, params, context, resolveInfo) => {
+ const { resource } = params
+ const session = context.driver.session()
+
+ if (resource && resource.length) {
+ await Promise.all(
+ resource.map(async node => {
+ await session.run(
+ `
+ MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
+ SET resource.deleted = true
+ RETURN author`,
+ {
+ userId: context.user.id,
+ },
+ )
+ }),
+ )
+ session.close()
+ }
+ return neo4jgraphql(object, params, context, resolveInfo, false)
+ },
+ },
+}
diff --git a/Human-Connection/backend/src/schema/resolvers/users.spec.js b/Human-Connection/backend/src/schema/resolvers/users.spec.js
new file mode 100644
index 000000000..9df5473bf
--- /dev/null
+++ b/Human-Connection/backend/src/schema/resolvers/users.spec.js
@@ -0,0 +1,278 @@
+import { GraphQLClient } from 'graphql-request'
+import { login, host } from '../../jest/helpers'
+import Factory from '../../seed/factories'
+import gql from 'graphql-tag'
+
+const factory = Factory()
+let client
+
+afterEach(async () => {
+ await factory.cleanDatabase()
+})
+
+describe('users', () => {
+ describe('CreateUser', () => {
+ const mutation = `
+ mutation($name: String, $password: String!, $email: String!) {
+ CreateUser(name: $name, password: $password, email: $email) {
+ id
+ }
+ }
+ `
+ describe('given valid password and email', () => {
+ const variables = {
+ name: 'John Doe',
+ password: '123',
+ email: '123@123.de',
+ }
+
+ describe('unauthenticated', () => {
+ beforeEach(async () => {
+ client = new GraphQLClient(host)
+ })
+
+ it('is not allowed to create users', async () => {
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('authenticated admin', () => {
+ beforeEach(async () => {
+ const adminParams = {
+ role: 'admin',
+ email: 'admin@example.org',
+ password: '1234',
+ }
+ await factory.create('User', adminParams)
+ const headers = await login(adminParams)
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('is allowed to create new users', async () => {
+ const expected = {
+ CreateUser: {
+ id: expect.any(String),
+ },
+ }
+ await expect(client.request(mutation, variables)).resolves.toEqual(expected)
+ })
+ })
+ })
+ })
+
+ describe('UpdateUser', () => {
+ const userParams = {
+ email: 'user@example.org',
+ password: '1234',
+ id: 'u47',
+ name: 'John Doe',
+ }
+ const variables = {
+ id: 'u47',
+ name: 'John Doughnut',
+ }
+
+ const mutation = `
+ mutation($id: ID!, $name: String) {
+ UpdateUser(id: $id, name: $name) {
+ id
+ name
+ }
+ }
+ `
+
+ beforeEach(async () => {
+ await factory.create('User', userParams)
+ })
+
+ describe('as another user', () => {
+ beforeEach(async () => {
+ const someoneElseParams = {
+ email: 'someoneElse@example.org',
+ password: '1234',
+ name: 'James Doe',
+ }
+
+ await factory.create('User', someoneElseParams)
+ const headers = await login(someoneElseParams)
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('is not allowed to change other user accounts', async () => {
+ await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
+ })
+ })
+
+ describe('as the same user', () => {
+ beforeEach(async () => {
+ const headers = await login(userParams)
+ client = new GraphQLClient(host, { headers })
+ })
+
+ it('name within specifications', async () => {
+ const expected = {
+ UpdateUser: {
+ id: 'u47',
+ name: 'John Doughnut',
+ },
+ }
+ await expect(client.request(mutation, variables)).resolves.toEqual(expected)
+ })
+
+ it('with no name', async () => {
+ const variables = {
+ id: 'u47',
+ name: null,
+ }
+ const expected = 'Username must be at least 3 characters long!'
+ await expect(client.request(mutation, variables)).rejects.toThrow(expected)
+ })
+
+ it('with too short name', async () => {
+ const variables = {
+ id: 'u47',
+ name: ' ',
+ }
+ const expected = 'Username must be at least 3 characters long!'
+ await expect(client.request(mutation, variables)).rejects.toThrow(expected)
+ })
+ })
+ })
+
+ describe('DeleteUser', () => {
+ let deleteUserVariables
+ let asAuthor
+ const deleteUserMutation = gql`
+ mutation($id: ID!, $resource: [Deletable]) {
+ DeleteUser(id: $id, resource: $resource) {
+ id
+ contributions {
+ id
+ deleted
+ }
+ comments {
+ id
+ deleted
+ }
+ }
+ }
+ `
+ beforeEach(async () => {
+ asAuthor = await factory.create('User', {
+ email: 'test@example.org',
+ password: '1234',
+ id: 'u343',
+ })
+ await factory.create('User', {
+ email: 'friendsAccount@example.org',
+ password: '1234',
+ id: 'u565',
+ })
+ deleteUserVariables = { id: 'u343', resource: [] }
+ })
+
+ describe('unauthenticated', () => {
+ it('throws authorization error', async () => {
+ client = new GraphQLClient(host)
+ await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow(
+ 'Not Authorised',
+ )
+ })
+ })
+
+ describe('authenticated', () => {
+ let headers
+ beforeEach(async () => {
+ headers = await login({
+ email: 'test@example.org',
+ password: '1234',
+ })
+ client = new GraphQLClient(host, { headers })
+ })
+
+ describe("attempting to delete another user's account", () => {
+ it('throws an authorization error', async () => {
+ deleteUserVariables = { id: 'u565' }
+ await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow(
+ 'Not Authorised',
+ )
+ })
+ })
+
+ describe('attempting to delete my own account', () => {
+ let expectedResponse
+ beforeEach(async () => {
+ await asAuthor.authenticateAs({
+ email: 'test@example.org',
+ password: '1234',
+ })
+ await asAuthor.create('Post', {
+ id: 'p139',
+ content: 'Post by user u343',
+ })
+ await asAuthor.create('Comment', {
+ id: 'c155',
+ postId: 'p139',
+ content: 'Comment by user u343',
+ })
+ expectedResponse = {
+ DeleteUser: {
+ id: 'u343',
+ contributions: [{ id: 'p139', deleted: false }],
+ comments: [{ id: 'c155', deleted: false }],
+ },
+ }
+ })
+ it("deletes my account, but doesn't delete posts or comments by default", async () => {
+ await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
+ expectedResponse,
+ )
+ })
+
+ describe("deletes a user's", () => {
+ it('posts on request', async () => {
+ deleteUserVariables = { id: 'u343', resource: ['Post'] }
+ expectedResponse = {
+ DeleteUser: {
+ id: 'u343',
+ contributions: [{ id: 'p139', deleted: true }],
+ comments: [{ id: 'c155', deleted: false }],
+ },
+ }
+ await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
+ expectedResponse,
+ )
+ })
+
+ it('comments on request', async () => {
+ deleteUserVariables = { id: 'u343', resource: ['Comment'] }
+ expectedResponse = {
+ DeleteUser: {
+ id: 'u343',
+ contributions: [{ id: 'p139', deleted: false }],
+ comments: [{ id: 'c155', deleted: true }],
+ },
+ }
+ await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
+ expectedResponse,
+ )
+ })
+
+ it('posts and comments on request', async () => {
+ deleteUserVariables = { id: 'u343', resource: ['Post', 'Comment'] }
+ expectedResponse = {
+ DeleteUser: {
+ id: 'u343',
+ contributions: [{ id: 'p139', deleted: true }],
+ comments: [{ id: 'c155', deleted: true }],
+ },
+ }
+ await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
+ expectedResponse,
+ )
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/backend/src/schema/types/enum/BadgeStatus.gql b/Human-Connection/backend/src/schema/types/enum/BadgeStatus.gql
new file mode 100644
index 000000000..b109663b3
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/enum/BadgeStatus.gql
@@ -0,0 +1,4 @@
+enum BadgeStatus {
+ permanent
+ temporary
+}
\ No newline at end of file
diff --git a/Human-Connection/backend/src/schema/types/enum/BadgeType.gql b/Human-Connection/backend/src/schema/types/enum/BadgeType.gql
new file mode 100644
index 000000000..eccf2e661
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/enum/BadgeType.gql
@@ -0,0 +1,4 @@
+enum BadgeType {
+ role
+ crowdfunding
+}
\ No newline at end of file
diff --git a/Human-Connection/backend/src/schema/types/enum/UserGroup.gql b/Human-Connection/backend/src/schema/types/enum/UserGroup.gql
new file mode 100644
index 000000000..af25bcc69
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/enum/UserGroup.gql
@@ -0,0 +1,5 @@
+enum UserGroup {
+ admin
+ moderator
+ user
+}
\ No newline at end of file
diff --git a/Human-Connection/backend/src/schema/types/enum/Visibility.gql b/Human-Connection/backend/src/schema/types/enum/Visibility.gql
new file mode 100644
index 000000000..4f9d5591a
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/enum/Visibility.gql
@@ -0,0 +1,5 @@
+enum Visibility {
+ public
+ friends
+ private
+}
\ No newline at end of file
diff --git a/Human-Connection/backend/src/schema/types/index.js b/Human-Connection/backend/src/schema/types/index.js
new file mode 100644
index 000000000..bcdceed44
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/index.js
@@ -0,0 +1,30 @@
+import fs from 'fs'
+import path from 'path'
+import { mergeTypes } from 'merge-graphql-schemas'
+
+const findGqlFiles = dir => {
+ var results = []
+ var list = fs.readdirSync(dir)
+ list.forEach(file => {
+ file = path.join(dir, file).toString('utf-8')
+ var stat = fs.statSync(file)
+ if (stat && stat.isDirectory()) {
+ // Recurse into a subdirectory
+ results = results.concat(findGqlFiles(file))
+ } else {
+ if (path.extname(file) === '.gql') {
+ // Is a gql file
+ results.push(file)
+ }
+ }
+ })
+ return results
+}
+
+let typeDefs = []
+
+findGqlFiles(__dirname).forEach(file => {
+ typeDefs.push(fs.readFileSync(file).toString('utf-8'))
+})
+
+export default mergeTypes(typeDefs, { all: true })
diff --git a/Human-Connection/backend/src/schema/types/scalar/Date.gql_ b/Human-Connection/backend/src/schema/types/scalar/Date.gql_
new file mode 100644
index 000000000..7b0004ea3
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/scalar/Date.gql_
@@ -0,0 +1 @@
+scalar Date
\ No newline at end of file
diff --git a/Human-Connection/backend/src/schema/types/scalar/DateTime.gql_ b/Human-Connection/backend/src/schema/types/scalar/DateTime.gql_
new file mode 100644
index 000000000..af973932f
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/scalar/DateTime.gql_
@@ -0,0 +1 @@
+scalar DateTime
\ No newline at end of file
diff --git a/Human-Connection/backend/src/schema/types/scalar/Time.gql_ b/Human-Connection/backend/src/schema/types/scalar/Time.gql_
new file mode 100644
index 000000000..53becdd66
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/scalar/Time.gql_
@@ -0,0 +1 @@
+scalar Time
\ No newline at end of file
diff --git a/Human-Connection/backend/src/schema/types/scalar/Upload.gql b/Human-Connection/backend/src/schema/types/scalar/Upload.gql
new file mode 100644
index 000000000..cf3965846
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/scalar/Upload.gql
@@ -0,0 +1 @@
+scalar Upload
diff --git a/Human-Connection/backend/src/schema/types/schema.gql b/Human-Connection/backend/src/schema/types/schema.gql
new file mode 100644
index 000000000..8b0f422c8
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/schema.gql
@@ -0,0 +1,143 @@
+type Query {
+ isLoggedIn: Boolean!
+ # Get the currently logged in User based on the given JWT Token
+ currentUser: User
+ # Get the latest Network Statistics
+ statistics: Statistics!
+ findPosts(filter: String!, limit: Int = 10): [Post]!
+ @cypher(
+ statement: """
+ CALL db.index.fulltext.queryNodes('full_text_search', $filter)
+ YIELD node as post, score
+ MATCH (post)<-[:WROTE]-(user:User)
+ WHERE score >= 0.2
+ AND NOT user.deleted = true AND NOT user.disabled = true
+ AND NOT post.deleted = true AND NOT post.disabled = true
+ RETURN post
+ LIMIT $limit
+ """
+ )
+ CommentByPost(postId: ID!): [Comment]!
+}
+
+type Mutation {
+ # Get a JWT Token for the given Email and password
+ login(email: String!, password: String!): String!
+ signup(email: String!, password: String!): Boolean!
+ changePassword(oldPassword: String!, newPassword: String!): String!
+ requestPasswordReset(email: String!): Boolean!
+ resetPassword(email: String!, code: String!, newPassword: String!): Boolean!
+ report(id: ID!, description: String): Report
+ disable(id: ID!): ID
+ enable(id: ID!): ID
+ reward(fromBadgeId: ID!, toUserId: ID!): ID
+ unreward(fromBadgeId: ID!, toUserId: ID!): ID
+ # Shout the given Type and ID
+ shout(id: ID!, type: ShoutTypeEnum): Boolean!
+ # Unshout the given Type and ID
+ unshout(id: ID!, type: ShoutTypeEnum): Boolean!
+ # Follow the given Type and ID
+ follow(id: ID!, type: FollowTypeEnum): Boolean!
+ # Unfollow the given Type and ID
+ unfollow(id: ID!, type: FollowTypeEnum): Boolean!
+ DeleteUser(id: ID!, resource: [Deletable]): User
+}
+
+type Statistics {
+ countUsers: Int!
+ countPosts: Int!
+ countComments: Int!
+ countNotifications: Int!
+ countOrganizations: Int!
+ countProjects: Int!
+ countInvites: Int!
+ countFollows: Int!
+ countShouts: Int!
+}
+
+type Notification {
+ id: ID!
+ read: Boolean
+ user: User @relation(name: "NOTIFIED", direction: "OUT")
+ post: Post @relation(name: "NOTIFIED", direction: "IN")
+ createdAt: String
+}
+
+type Location {
+ id: ID!
+ name: String!
+ nameEN: String
+ nameDE: String
+ nameFR: String
+ nameNL: String
+ nameIT: String
+ nameES: String
+ namePT: String
+ namePL: String
+ type: String!
+ lat: Float
+ lng: Float
+ parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
+}
+
+type Report {
+ id: ID!
+ submitter: User @relation(name: "REPORTED", direction: "IN")
+ description: String
+ type: String!
+ @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]")
+ createdAt: String
+ comment: Comment @relation(name: "REPORTED", direction: "OUT")
+ post: Post @relation(name: "REPORTED", direction: "OUT")
+ user: User @relation(name: "REPORTED", direction: "OUT")
+}
+
+enum Deletable {
+ Post
+ Comment
+}
+
+enum ShoutTypeEnum {
+ Post
+ Organization
+ Project
+}
+enum FollowTypeEnum {
+ User
+ Organization
+ Project
+}
+
+type Reward {
+ id: ID!
+ user: User @relation(name: "REWARDED", direction: "IN")
+ rewarderId: ID
+ createdAt: String
+ badge: Badge @relation(name: "REWARDED", direction: "OUT")
+}
+
+type Organization {
+ id: ID!
+ createdBy: User @relation(name: "CREATED_ORGA", direction: "IN")
+ ownedBy: [User] @relation(name: "OWNING_ORGA", direction: "IN")
+ name: String!
+ slug: String
+ description: String!
+ descriptionExcerpt: String
+ deleted: Boolean
+ disabled: Boolean
+
+ tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
+ categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
+}
+
+type SharedInboxEndpoint {
+ id: ID!
+ uri: String
+}
+
+type SocialMedia {
+ id: ID!
+ url: String
+ ownedBy: [User]! @relation(name: "OWNED", direction: "IN")
+}
diff --git a/Human-Connection/backend/src/schema/types/schema_full.gql_ b/Human-Connection/backend/src/schema/types/schema_full.gql_
new file mode 100644
index 000000000..a581d287c
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/schema_full.gql_
@@ -0,0 +1,324 @@
+scalar Upload
+
+type Query {
+ isLoggedIn: Boolean!
+ # Get the currently logged in User based on the given JWT Token
+ currentUser: User
+ # Get the latest Network Statistics
+ statistics: Statistics!
+ findPosts(filter: String!, limit: Int = 10): [Post]! @cypher(
+ statement: """
+ CALL db.index.fulltext.queryNodes('full_text_search', $filter)
+ YIELD node as post, score
+ MATCH (post)<-[:WROTE]-(user:User)
+ WHERE score >= 0.2
+ AND NOT user.deleted = true AND NOT user.disabled = true
+ AND NOT post.deleted = true AND NOT post.disabled = true
+ RETURN post
+ LIMIT $limit
+ """
+ )
+ CommentByPost(postId: ID!): [Comment]!
+}
+
+type Mutation {
+ # Get a JWT Token for the given Email and password
+ login(email: String!, password: String!): String!
+ signup(email: String!, password: String!): Boolean!
+ changePassword(oldPassword:String!, newPassword: String!): String!
+ report(id: ID!, description: String): Report
+ disable(id: ID!): ID
+ enable(id: ID!): ID
+ reward(fromBadgeId: ID!, toUserId: ID!): ID
+ unreward(fromBadgeId: ID!, toUserId: ID!): ID
+ # Shout the given Type and ID
+ shout(id: ID!, type: ShoutTypeEnum): Boolean!
+ # Unshout the given Type and ID
+ unshout(id: ID!, type: ShoutTypeEnum): Boolean!
+ # Follow the given Type and ID
+ follow(id: ID!, type: FollowTypeEnum): Boolean!
+ # Unfollow the given Type and ID
+ unfollow(id: ID!, type: FollowTypeEnum): Boolean!
+}
+
+type Statistics {
+ countUsers: Int!
+ countPosts: Int!
+ countComments: Int!
+ countNotifications: Int!
+ countOrganizations: Int!
+ countProjects: Int!
+ countInvites: Int!
+ countFollows: Int!
+ countShouts: Int!
+}
+
+type Notification {
+ id: ID!
+ read: Boolean,
+ user: User @relation(name: "NOTIFIED", direction: "OUT")
+ post: Post @relation(name: "NOTIFIED", direction: "IN")
+ createdAt: String
+}
+
+scalar Date
+scalar Time
+scalar DateTime
+
+enum VisibilityEnum {
+ public
+ friends
+ private
+}
+
+enum UserGroupEnum {
+ admin
+ moderator
+ user
+}
+
+type Location {
+ id: ID!
+ name: String!
+ nameEN: String
+ nameDE: String
+ nameFR: String
+ nameNL: String
+ nameIT: String
+ nameES: String
+ namePT: String
+ namePL: String
+ type: String!
+ lat: Float
+ lng: Float
+ parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
+}
+
+type User {
+ id: ID!
+ actorId: String
+ name: String
+ email: String!
+ slug: String
+ password: String!
+ avatar: String
+ avatarUpload: Upload
+ deleted: Boolean
+ disabled: Boolean
+ disabledBy: User @relation(name: "DISABLED", direction: "IN")
+ role: UserGroupEnum
+ publicKey: String
+ privateKey: String
+
+ location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
+ locationName: String
+ about: String
+ socialMedia: [SocialMedia]! @relation(name: "OWNED", direction: "OUT")
+
+ createdAt: String
+ updatedAt: String
+
+ notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
+
+ friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
+ friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
+
+ following: [User]! @relation(name: "FOLLOWS", direction: "OUT")
+ followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)")
+
+ followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
+ followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
+
+ # Is the currently logged in user following that user?
+ followedByCurrentUser: Boolean! @cypher(
+ statement: """
+ MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId})
+ RETURN COUNT(u) >= 1
+ """
+ )
+
+ #contributions: [WrittenPost]!
+ #contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
+ # @cypher(
+ # statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp"
+ # )
+ contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
+ contributionsCount: Int! @cypher(
+ statement: """
+ MATCH (this)-[:WROTE]->(r:Post)
+ WHERE (NOT exists(r.deleted) OR r.deleted = false)
+ AND (NOT exists(r.disabled) OR r.disabled = false)
+ RETURN COUNT(r)
+ """
+ )
+
+ comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
+ commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
+
+ shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
+ shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
+
+ organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT")
+ organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT")
+
+ blacklisted: [User]! @relation(name: "BLACKLISTED", direction: "OUT")
+
+ categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
+
+ badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
+ badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
+}
+
+type Post {
+ id: ID!
+ activityId: String
+ objectId: String
+ author: User @relation(name: "WROTE", direction: "IN")
+ title: String!
+ slug: String
+ content: String!
+ contentExcerpt: String
+ image: String
+ imageUpload: Upload
+ visibility: VisibilityEnum
+ deleted: Boolean
+ disabled: Boolean
+ disabledBy: User @relation(name: "DISABLED", direction: "IN")
+ createdAt: String
+ updatedAt: String
+
+ relatedContributions: [Post]! @cypher(
+ statement: """
+ MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
+ RETURN DISTINCT post
+ LIMIT 10
+ """
+ )
+
+ tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
+ categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
+
+ comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
+ commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
+
+ shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
+ shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
+
+ # Has the currently logged in user shouted that post?
+ shoutedByCurrentUser: Boolean! @cypher(
+ statement: """
+ MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
+ RETURN COUNT(u) >= 1
+ """
+ )
+}
+
+type Comment {
+ id: ID!
+ activityId: String
+ postId: ID
+ author: User @relation(name: "WROTE", direction: "IN")
+ content: String!
+ contentExcerpt: String
+ post: Post @relation(name: "COMMENTS", direction: "OUT")
+ createdAt: String
+ updatedAt: String
+ deleted: Boolean
+ disabled: Boolean
+ disabledBy: User @relation(name: "DISABLED", direction: "IN")
+}
+
+type Report {
+ id: ID!
+ submitter: User @relation(name: "REPORTED", direction: "IN")
+ description: String
+ type: String! @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]")
+ createdAt: String
+ comment: Comment @relation(name: "REPORTED", direction: "OUT")
+ post: Post @relation(name: "REPORTED", direction: "OUT")
+ user: User @relation(name: "REPORTED", direction: "OUT")
+}
+
+type Category {
+ id: ID!
+ name: String!
+ slug: String
+ icon: String!
+ posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN")
+ postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)")
+}
+
+type Badge {
+ id: ID!
+ key: String!
+ type: BadgeTypeEnum!
+ status: BadgeStatusEnum!
+ icon: String!
+
+ rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
+}
+
+enum BadgeTypeEnum {
+ role
+ crowdfunding
+}
+enum BadgeStatusEnum {
+ permanent
+ temporary
+}
+enum ShoutTypeEnum {
+ Post
+ Organization
+ Project
+}
+enum FollowTypeEnum {
+ User
+ Organization
+ Project
+}
+
+type Reward {
+ id: ID!
+ user: User @relation(name: "REWARDED", direction: "IN")
+ rewarderId: ID
+ createdAt: String
+ badge: Badge @relation(name: "REWARDED", direction: "OUT")
+}
+
+type Organization {
+ id: ID!
+ createdBy: User @relation(name: "CREATED_ORGA", direction: "IN")
+ ownedBy: [User] @relation(name: "OWNING_ORGA", direction: "IN")
+ name: String!
+ slug: String
+ description: String!
+ descriptionExcerpt: String
+ deleted: Boolean
+ disabled: Boolean
+
+ tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
+ categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
+}
+
+type Tag {
+ id: ID!
+ name: String!
+ taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
+ taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN")
+ taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)")
+ taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)")
+ deleted: Boolean
+ disabled: Boolean
+}
+
+type SharedInboxEndpoint {
+ id: ID!
+ uri: String
+}
+
+type SocialMedia {
+ id: ID!
+ url: String
+ ownedBy: [User]! @relation(name: "OWNED", direction: "IN")
+}
+
diff --git a/Human-Connection/backend/src/schema/types/type/Badge.gql b/Human-Connection/backend/src/schema/types/type/Badge.gql
new file mode 100644
index 000000000..68c5d5707
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/type/Badge.gql
@@ -0,0 +1,13 @@
+type Badge {
+ id: ID!
+ key: String!
+ type: BadgeType!
+ status: BadgeStatus!
+ icon: String!
+ #createdAt: DateTime
+ #updatedAt: DateTime
+ createdAt: String
+ updatedAt: String
+
+ rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
+}
\ No newline at end of file
diff --git a/Human-Connection/backend/src/schema/types/type/Category.gql b/Human-Connection/backend/src/schema/types/type/Category.gql
new file mode 100644
index 000000000..5920ebbdb
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/type/Category.gql
@@ -0,0 +1,13 @@
+type Category {
+ id: ID!
+ name: String!
+ slug: String
+ icon: String!
+ #createdAt: DateTime
+ #updatedAt: DateTime
+ createdAt: String
+ updatedAt: String
+
+ posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN")
+ postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)")
+}
\ No newline at end of file
diff --git a/Human-Connection/backend/src/schema/types/type/Comment.gql b/Human-Connection/backend/src/schema/types/type/Comment.gql
new file mode 100644
index 000000000..441fba179
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/type/Comment.gql
@@ -0,0 +1,33 @@
+type Comment {
+ id: ID!
+ activityId: String
+ author: User @relation(name: "WROTE", direction: "IN")
+ content: String!
+ contentExcerpt: String
+ post: Post @relation(name: "COMMENTS", direction: "OUT")
+ createdAt: String
+ updatedAt: String
+ deleted: Boolean
+ disabled: Boolean
+ disabledBy: User @relation(name: "DISABLED", direction: "IN")
+}
+
+type Mutation {
+ CreateComment(
+ id: ID
+ postId: ID!
+ content: String!
+ contentExcerpt: String
+ deleted: Boolean
+ disabled: Boolean
+ createdAt: String
+ ): Comment
+ UpdateComment(
+ id: ID!
+ content: String
+ contentExcerpt: String
+ deleted: Boolean
+ disabled: Boolean
+ ): Comment
+ DeleteComment(id: ID!): Comment
+}
diff --git a/Human-Connection/backend/src/schema/types/type/Post.gql b/Human-Connection/backend/src/schema/types/type/Post.gql
new file mode 100644
index 000000000..deb1d8f85
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/type/Post.gql
@@ -0,0 +1,90 @@
+type Post {
+ id: ID!
+ activityId: String
+ objectId: String
+ author: User @relation(name: "WROTE", direction: "IN")
+ title: String!
+ slug: String
+ content: String!
+ contentExcerpt: String
+ image: String
+ imageUpload: Upload
+ visibility: Visibility
+ deleted: Boolean
+ disabled: Boolean
+ disabledBy: User @relation(name: "DISABLED", direction: "IN")
+ createdAt: String
+ updatedAt: String
+ language: String
+ relatedContributions: [Post]!
+ @cypher(
+ statement: """
+ MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
+ RETURN DISTINCT post
+ LIMIT 10
+ """
+ )
+
+ tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
+ categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
+
+ comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
+ commentsCount: Int!
+ @cypher(
+ statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)"
+ )
+
+ shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
+ shoutedCount: Int!
+ @cypher(
+ statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)"
+ )
+
+ # Has the currently logged in user shouted that post?
+ shoutedByCurrentUser: Boolean!
+ @cypher(
+ statement: """
+ MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
+ RETURN COUNT(u) >= 1
+ """
+ )
+}
+
+type Mutation {
+ CreatePost(
+ id: ID
+ activityId: String
+ objectId: String
+ title: String!
+ slug: String
+ content: String!
+ image: String
+ imageUpload: Upload
+ visibility: Visibility
+ deleted: Boolean
+ disabled: Boolean
+ createdAt: String
+ updatedAt: String
+ language: String
+ categoryIds: [ID]
+ contentExcerpt: String
+ ): Post
+ UpdatePost(
+ id: ID!
+ activityId: String
+ objectId: String
+ title: String!
+ slug: String
+ content: String!
+ contentExcerpt: String
+ image: String
+ imageUpload: Upload
+ visibility: Visibility
+ deleted: Boolean
+ disabled: Boolean
+ createdAt: String
+ updatedAt: String
+ language: String
+ categoryIds: [ID]
+ ): Post
+}
diff --git a/Human-Connection/backend/src/schema/types/type/Tag.gql b/Human-Connection/backend/src/schema/types/type/Tag.gql
new file mode 100644
index 000000000..ecbd0b46a
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/type/Tag.gql
@@ -0,0 +1,10 @@
+type Tag {
+ id: ID!
+ name: String!
+ taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
+ taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN")
+ taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)")
+ taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)")
+ deleted: Boolean
+ disabled: Boolean
+}
\ No newline at end of file
diff --git a/Human-Connection/backend/src/schema/types/type/User.gql b/Human-Connection/backend/src/schema/types/type/User.gql
new file mode 100644
index 000000000..6836f16fe
--- /dev/null
+++ b/Human-Connection/backend/src/schema/types/type/User.gql
@@ -0,0 +1,80 @@
+type User {
+ id: ID!
+ actorId: String
+ name: String
+ email: String!
+ slug: String
+ password: String!
+ avatar: String
+ coverImg: String
+ avatarUpload: Upload
+ deleted: Boolean
+ disabled: Boolean
+ disabledBy: User @relation(name: "DISABLED", direction: "IN")
+ role: UserGroup
+ publicKey: String
+ privateKey: String
+
+ wasInvited: Boolean
+ wasSeeded: Boolean
+
+ location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
+ locationName: String
+ about: String
+ socialMedia: [SocialMedia]! @relation(name: "OWNED", direction: "OUT")
+
+ #createdAt: DateTime
+ #updatedAt: DateTime
+ createdAt: String
+ updatedAt: String
+
+ notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
+
+ friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
+ friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
+
+ following: [User]! @relation(name: "FOLLOWS", direction: "OUT")
+ followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)")
+
+ followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
+ followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
+
+ # Is the currently logged in user following that user?
+ followedByCurrentUser: Boolean! @cypher(
+ statement: """
+ MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId})
+ RETURN COUNT(u) >= 1
+ """
+ )
+
+ #contributions: [WrittenPost]!
+ #contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
+ # @cypher(
+ # statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp"
+ # )
+ contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
+ contributionsCount: Int! @cypher(
+ statement: """
+ MATCH (this)-[:WROTE]->(r:Post)
+ WHERE NOT r.deleted = true AND NOT r.disabled = true
+ RETURN COUNT(r)
+ """
+ )
+
+ comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
+ commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
+ commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment)-[:COMMENTS]->(p:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true AND NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))")
+
+ shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
+ shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
+
+ organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT")
+ organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT")
+
+ blacklisted: [User]! @relation(name: "BLACKLISTED", direction: "OUT")
+
+ categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
+
+ badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
+ badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
+}
diff --git a/Human-Connection/backend/src/seed/factories/badges.js b/Human-Connection/backend/src/seed/factories/badges.js
new file mode 100644
index 000000000..6414e9f36
--- /dev/null
+++ b/Human-Connection/backend/src/seed/factories/badges.js
@@ -0,0 +1,28 @@
+import uuid from 'uuid/v4'
+
+export default function(params) {
+ const {
+ id = uuid(),
+ key = '',
+ type = 'crowdfunding',
+ status = 'permanent',
+ icon = '/img/badges/indiegogo_en_panda.svg',
+ } = params
+
+ return {
+ mutation: `
+ mutation(
+ $id: ID
+ $key: String!
+ $type: BadgeType!
+ $status: BadgeStatus!
+ $icon: String!
+ ) {
+ CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
+ id
+ }
+ }
+ `,
+ variables: { id, key, type, status, icon },
+ }
+}
diff --git a/Human-Connection/backend/src/seed/factories/categories.js b/Human-Connection/backend/src/seed/factories/categories.js
new file mode 100644
index 000000000..341f1b1fd
--- /dev/null
+++ b/Human-Connection/backend/src/seed/factories/categories.js
@@ -0,0 +1,17 @@
+import uuid from 'uuid/v4'
+
+export default function(params) {
+ const { id = uuid(), name, slug, icon } = params
+
+ return {
+ mutation: `
+ mutation($id: ID, $name: String!, $slug: String, $icon: String!) {
+ CreateCategory(id: $id, name: $name, slug: $slug, icon: $icon) {
+ id
+ name
+ }
+ }
+ `,
+ variables: { id, name, slug, icon },
+ }
+}
diff --git a/Human-Connection/backend/src/seed/factories/comments.js b/Human-Connection/backend/src/seed/factories/comments.js
new file mode 100644
index 000000000..20933e947
--- /dev/null
+++ b/Human-Connection/backend/src/seed/factories/comments.js
@@ -0,0 +1,21 @@
+import faker from 'faker'
+import uuid from 'uuid/v4'
+
+export default function(params) {
+ const {
+ id = uuid(),
+ postId = 'p6',
+ content = [faker.lorem.sentence(), faker.lorem.sentence()].join('. '),
+ } = params
+
+ return {
+ mutation: `
+ mutation($id: ID!, $postId: ID!, $content: String!) {
+ CreateComment(id: $id, postId: $postId, content: $content) {
+ id
+ }
+ }
+ `,
+ variables: { id, postId, content },
+ }
+}
diff --git a/Human-Connection/backend/src/seed/factories/index.js b/Human-Connection/backend/src/seed/factories/index.js
new file mode 100644
index 000000000..211edf87e
--- /dev/null
+++ b/Human-Connection/backend/src/seed/factories/index.js
@@ -0,0 +1,126 @@
+import { GraphQLClient, request } from 'graphql-request'
+import { getDriver } from '../../bootstrap/neo4j'
+import createBadge from './badges.js'
+import createUser from './users.js'
+import createOrganization from './organizations.js'
+import createPost from './posts.js'
+import createComment from './comments.js'
+import createCategory from './categories.js'
+import createTag from './tags.js'
+import createReport from './reports.js'
+import createNotification from './notifications.js'
+
+export const seedServerHost = 'http://127.0.0.1:4001'
+
+const authenticatedHeaders = async ({ email, password }, host) => {
+ const mutation = `
+ mutation {
+ login(email:"${email}", password:"${password}")
+ }`
+ const response = await request(host, mutation)
+ return {
+ authorization: `Bearer ${response.login}`,
+ }
+}
+const factories = {
+ Badge: createBadge,
+ User: createUser,
+ Organization: createOrganization,
+ Post: createPost,
+ Comment: createComment,
+ Category: createCategory,
+ Tag: createTag,
+ Report: createReport,
+ Notification: createNotification,
+}
+
+export const cleanDatabase = async (options = {}) => {
+ const { driver = getDriver() } = options
+ const session = driver.session()
+ const cypher = 'MATCH (n) DETACH DELETE n'
+ try {
+ return await session.run(cypher)
+ } catch (error) {
+ throw error
+ } finally {
+ session.close()
+ }
+}
+
+export default function Factory(options = {}) {
+ const { neo4jDriver = getDriver(), seedServerHost = 'http://127.0.0.1:4001' } = options
+
+ const graphQLClient = new GraphQLClient(seedServerHost)
+
+ const result = {
+ neo4jDriver,
+ seedServerHost,
+ graphQLClient,
+ factories,
+ lastResponse: null,
+ async authenticateAs({ email, password }) {
+ const headers = await authenticatedHeaders({ email, password }, seedServerHost)
+ this.lastResponse = headers
+ this.graphQLClient = new GraphQLClient(seedServerHost, { headers })
+ return this
+ },
+ async create(node, properties) {
+ const { mutation, variables } = this.factories[node](properties)
+ this.lastResponse = await this.graphQLClient.request(mutation, variables)
+ return this
+ },
+ async relate(node, relationship, properties) {
+ const { from, to } = properties
+ const mutation = `
+ mutation {
+ Add${node}${relationship}(
+ from: { id: "${from}" },
+ to: { id: "${to}" }
+ ) { from { id } }
+ }
+ `
+ this.lastResponse = await this.graphQLClient.request(mutation)
+ return this
+ },
+ async mutate(mutation, variables) {
+ this.lastResponse = await this.graphQLClient.request(mutation, variables)
+ return this
+ },
+ async shout(properties) {
+ const { id, type } = properties
+ const mutation = `
+ mutation {
+ shout(
+ id: "${id}",
+ type: ${type}
+ )
+ }
+ `
+ this.lastResponse = await this.graphQLClient.request(mutation)
+ return this
+ },
+ async follow(properties) {
+ const { id, type } = properties
+ const mutation = `
+ mutation {
+ follow(
+ id: "${id}",
+ type: ${type}
+ )
+ }
+ `
+ this.lastResponse = await this.graphQLClient.request(mutation)
+ return this
+ },
+ async cleanDatabase() {
+ this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver })
+ return this
+ },
+ }
+ result.authenticateAs.bind(result)
+ result.create.bind(result)
+ result.relate.bind(result)
+ result.mutate.bind(result)
+ result.cleanDatabase.bind(result)
+ return result
+}
diff --git a/Human-Connection/backend/src/seed/factories/notifications.js b/Human-Connection/backend/src/seed/factories/notifications.js
new file mode 100644
index 000000000..d14d4294a
--- /dev/null
+++ b/Human-Connection/backend/src/seed/factories/notifications.js
@@ -0,0 +1,17 @@
+import uuid from 'uuid/v4'
+
+export default function(params) {
+ const { id = uuid(), read = false } = params
+
+ return {
+ mutation: `
+ mutation($id: ID, $read: Boolean) {
+ CreateNotification(id: $id, read: $read) {
+ id
+ read
+ }
+ }
+ `,
+ variables: { id, read },
+ }
+}
diff --git a/Human-Connection/backend/src/seed/factories/organizations.js b/Human-Connection/backend/src/seed/factories/organizations.js
new file mode 100644
index 000000000..536de1597
--- /dev/null
+++ b/Human-Connection/backend/src/seed/factories/organizations.js
@@ -0,0 +1,21 @@
+import faker from 'faker'
+import uuid from 'uuid/v4'
+
+export default function create(params) {
+ const {
+ id = uuid(),
+ name = faker.company.companyName(),
+ description = faker.company.catchPhrase(),
+ } = params
+
+ return {
+ mutation: `
+ mutation($id: ID!, $name: String!, $description: String!) {
+ CreateOrganization(id: $id, name: $name, description: $description) {
+ name
+ }
+ }
+ `,
+ variables: { id, name, description },
+ }
+}
diff --git a/Human-Connection/backend/src/seed/factories/posts.js b/Human-Connection/backend/src/seed/factories/posts.js
new file mode 100644
index 000000000..b8e30ee2e
--- /dev/null
+++ b/Human-Connection/backend/src/seed/factories/posts.js
@@ -0,0 +1,48 @@
+import faker from 'faker'
+import uuid from 'uuid/v4'
+
+export default function(params) {
+ const {
+ id = uuid(),
+ slug = '',
+ title = faker.lorem.sentence(),
+ content = [
+ faker.lorem.sentence(),
+ faker.lorem.sentence(),
+ faker.lorem.sentence(),
+ faker.lorem.sentence(),
+ faker.lorem.sentence(),
+ ].join('. '),
+ image = faker.image.unsplash.imageUrl(),
+ visibility = 'public',
+ deleted = false,
+ } = params
+
+ return {
+ mutation: `
+ mutation(
+ $id: ID!
+ $slug: String
+ $title: String!
+ $content: String!
+ $image: String
+ $visibility: Visibility
+ $deleted: Boolean
+ ) {
+ CreatePost(
+ id: $id
+ slug: $slug
+ title: $title
+ content: $content
+ image: $image
+ visibility: $visibility
+ deleted: $deleted
+ ) {
+ title
+ content
+ }
+ }
+ `,
+ variables: { id, slug, title, content, image, visibility, deleted },
+ }
+}
diff --git a/Human-Connection/backend/src/seed/factories/reports.js b/Human-Connection/backend/src/seed/factories/reports.js
new file mode 100644
index 000000000..5bb6f6ba2
--- /dev/null
+++ b/Human-Connection/backend/src/seed/factories/reports.js
@@ -0,0 +1,19 @@
+import faker from 'faker'
+
+export default function create(params) {
+ const { description = faker.lorem.sentence(), id } = params
+
+ return {
+ mutation: `
+ mutation($id: ID!, $description: String!) {
+ report(description: $description, id: $id) {
+ id
+ }
+ }
+ `,
+ variables: {
+ id,
+ description,
+ },
+ }
+}
diff --git a/Human-Connection/backend/src/seed/factories/tags.js b/Human-Connection/backend/src/seed/factories/tags.js
new file mode 100644
index 000000000..15ded1986
--- /dev/null
+++ b/Human-Connection/backend/src/seed/factories/tags.js
@@ -0,0 +1,16 @@
+import uuid from 'uuid/v4'
+
+export default function(params) {
+ const { id = uuid(), name = '#human-connection' } = params
+
+ return {
+ mutation: `
+ mutation($id: ID!, $name: String!) {
+ CreateTag(id: $id, name: $name) {
+ name
+ }
+ }
+ `,
+ variables: { id, name },
+ }
+}
diff --git a/Human-Connection/backend/src/seed/factories/users.js b/Human-Connection/backend/src/seed/factories/users.js
new file mode 100644
index 000000000..ca17d1721
--- /dev/null
+++ b/Human-Connection/backend/src/seed/factories/users.js
@@ -0,0 +1,51 @@
+import faker from 'faker'
+import uuid from 'uuid/v4'
+
+export default function create(params) {
+ const {
+ id = uuid(),
+ name = faker.name.findName(),
+ slug = '',
+ email = faker.internet.email(),
+ password = '1234',
+ role = 'user',
+ avatar = faker.internet.avatar(),
+ about = faker.lorem.paragraph(),
+ } = params
+
+ return {
+ mutation: `
+ mutation(
+ $id: ID!
+ $name: String
+ $slug: String
+ $password: String!
+ $email: String!
+ $avatar: String
+ $about: String
+ $role: UserGroup
+ ) {
+ CreateUser(
+ id: $id
+ name: $name
+ slug: $slug
+ password: $password
+ email: $email
+ avatar: $avatar
+ about: $about
+ role: $role
+ ) {
+ id
+ name
+ slug
+ email
+ avatar
+ role
+ deleted
+ disabled
+ }
+ }
+ `,
+ variables: { id, name, slug, password, email, avatar, about, role },
+ }
+}
diff --git a/Human-Connection/backend/src/seed/reset-db.js b/Human-Connection/backend/src/seed/reset-db.js
new file mode 100644
index 000000000..125d135d8
--- /dev/null
+++ b/Human-Connection/backend/src/seed/reset-db.js
@@ -0,0 +1,16 @@
+import { cleanDatabase } from './factories'
+
+if (process.env.NODE_ENV === 'production') {
+ throw new Error(`You cannot clean the database in production environment!`)
+}
+
+;(async function() {
+ try {
+ await cleanDatabase()
+ console.log('Successfully deleted all nodes and relations!') // eslint-disable-line no-console
+ process.exit(0)
+ } catch (err) {
+ console.log(`Error occurred deleting the nodes and relations (reset the db)\n\n${err}`) // eslint-disable-line no-console
+ process.exit(1)
+ }
+})()
diff --git a/Human-Connection/backend/src/seed/seed-db.js b/Human-Connection/backend/src/seed/seed-db.js
new file mode 100644
index 000000000..27c07868d
--- /dev/null
+++ b/Human-Connection/backend/src/seed/seed-db.js
@@ -0,0 +1,358 @@
+import faker from 'faker'
+import Factory from './factories'
+
+/* eslint-disable no-multi-spaces */
+;(async function() {
+ try {
+ const f = Factory()
+ await Promise.all([
+ f.create('Badge', {
+ id: 'b1',
+ key: 'indiegogo_en_racoon',
+ type: 'crowdfunding',
+ status: 'permanent',
+ icon: '/img/badges/indiegogo_en_racoon.svg',
+ }),
+ f.create('Badge', {
+ id: 'b2',
+ key: 'indiegogo_en_rabbit',
+ type: 'crowdfunding',
+ status: 'permanent',
+ icon: '/img/badges/indiegogo_en_rabbit.svg',
+ }),
+ f.create('Badge', {
+ id: 'b3',
+ key: 'indiegogo_en_wolf',
+ type: 'crowdfunding',
+ status: 'permanent',
+ icon: '/img/badges/indiegogo_en_wolf.svg',
+ }),
+ f.create('Badge', {
+ id: 'b4',
+ key: 'indiegogo_en_bear',
+ type: 'crowdfunding',
+ status: 'permanent',
+ icon: '/img/badges/indiegogo_en_bear.svg',
+ }),
+ f.create('Badge', {
+ id: 'b5',
+ key: 'indiegogo_en_turtle',
+ type: 'crowdfunding',
+ status: 'permanent',
+ icon: '/img/badges/indiegogo_en_turtle.svg',
+ }),
+ f.create('Badge', {
+ id: 'b6',
+ key: 'indiegogo_en_rhino',
+ type: 'crowdfunding',
+ status: 'permanent',
+ icon: '/img/badges/indiegogo_en_rhino.svg',
+ }),
+ ])
+
+ await Promise.all([
+ f.create('User', {
+ id: 'u1',
+ name: 'Peter Lustig',
+ role: 'admin',
+ email: 'admin@example.org',
+ }),
+ f.create('User', {
+ id: 'u2',
+ name: 'Bob der Baumeister',
+ role: 'moderator',
+ email: 'moderator@example.org',
+ }),
+ f.create('User', {
+ id: 'u3',
+ name: 'Jenny Rostock',
+ role: 'user',
+ email: 'user@example.org',
+ }),
+ f.create('User', { id: 'u4', name: 'Tick', role: 'user', email: 'tick@example.org' }),
+ f.create('User', { id: 'u5', name: 'Trick', role: 'user', email: 'trick@example.org' }),
+ f.create('User', { id: 'u6', name: 'Track', role: 'user', email: 'track@example.org' }),
+ f.create('User', { id: 'u7', name: 'Dagobert', role: 'user', email: 'dagobert@example.org' }),
+ ])
+
+ const [asAdmin, asModerator, asUser, asTick, asTrick, asTrack] = await Promise.all([
+ Factory().authenticateAs({ email: 'admin@example.org', password: '1234' }),
+ Factory().authenticateAs({ email: 'moderator@example.org', password: '1234' }),
+ Factory().authenticateAs({ email: 'user@example.org', password: '1234' }),
+ Factory().authenticateAs({ email: 'tick@example.org', password: '1234' }),
+ Factory().authenticateAs({ email: 'trick@example.org', password: '1234' }),
+ Factory().authenticateAs({ email: 'track@example.org', password: '1234' }),
+ ])
+
+ await Promise.all([
+ f.relate('User', 'Badges', { from: 'b6', to: 'u1' }),
+ f.relate('User', 'Badges', { from: 'b5', to: 'u2' }),
+ f.relate('User', 'Badges', { from: 'b4', to: 'u3' }),
+ f.relate('User', 'Badges', { from: 'b3', to: 'u4' }),
+ f.relate('User', 'Badges', { from: 'b2', to: 'u5' }),
+ f.relate('User', 'Badges', { from: 'b1', to: 'u6' }),
+ f.relate('User', 'Friends', { from: 'u1', to: 'u2' }),
+ f.relate('User', 'Friends', { from: 'u1', to: 'u3' }),
+ f.relate('User', 'Friends', { from: 'u2', to: 'u3' }),
+ f.relate('User', 'Blacklisted', { from: 'u7', to: 'u4' }),
+ f.relate('User', 'Blacklisted', { from: 'u7', to: 'u5' }),
+ f.relate('User', 'Blacklisted', { from: 'u7', to: 'u6' }),
+ ])
+
+ await Promise.all([
+ asAdmin.follow({ id: 'u3', type: 'User' }),
+ asModerator.follow({ id: 'u4', type: 'User' }),
+ asUser.follow({ id: 'u4', type: 'User' }),
+ asTick.follow({ id: 'u6', type: 'User' }),
+ asTrick.follow({ id: 'u4', type: 'User' }),
+ asTrack.follow({ id: 'u3', type: 'User' }),
+ ])
+
+ await Promise.all([
+ f.create('Category', { id: 'cat1', name: 'Just For Fun', slug: 'justforfun', icon: 'smile' }),
+ f.create('Category', {
+ id: 'cat2',
+ name: 'Happyness & Values',
+ slug: 'happyness-values',
+ icon: 'heart-o',
+ }),
+ f.create('Category', {
+ id: 'cat3',
+ name: 'Health & Wellbeing',
+ slug: 'health-wellbeing',
+ icon: 'medkit',
+ }),
+ f.create('Category', {
+ id: 'cat4',
+ name: 'Environment & Nature',
+ slug: 'environment-nature',
+ icon: 'tree',
+ }),
+ f.create('Category', {
+ id: 'cat5',
+ name: 'Animal Protection',
+ slug: 'animalprotection',
+ icon: 'paw',
+ }),
+ f.create('Category', {
+ id: 'cat6',
+ name: 'Humanrights Justice',
+ slug: 'humanrights-justice',
+ icon: 'balance-scale',
+ }),
+ f.create('Category', {
+ id: 'cat7',
+ name: 'Education & Sciences',
+ slug: 'education-sciences',
+ icon: 'graduation-cap',
+ }),
+ f.create('Category', {
+ id: 'cat8',
+ name: 'Cooperation & Development',
+ slug: 'cooperation-development',
+ icon: 'users',
+ }),
+ f.create('Category', {
+ id: 'cat9',
+ name: 'Democracy & Politics',
+ slug: 'democracy-politics',
+ icon: 'university',
+ }),
+ f.create('Category', {
+ id: 'cat10',
+ name: 'Economy & Finances',
+ slug: 'economy-finances',
+ icon: 'money',
+ }),
+ f.create('Category', {
+ id: 'cat11',
+ name: 'Energy & Technology',
+ slug: 'energy-technology',
+ icon: 'flash',
+ }),
+ f.create('Category', {
+ id: 'cat12',
+ name: 'IT, Internet & Data Privacy',
+ slug: 'it-internet-dataprivacy',
+ icon: 'mouse-pointer',
+ }),
+ f.create('Category', {
+ id: 'cat13',
+ name: 'Art, Curlure & Sport',
+ slug: 'art-culture-sport',
+ icon: 'paint-brush',
+ }),
+ f.create('Category', {
+ id: 'cat14',
+ name: 'Freedom of Speech',
+ slug: 'freedomofspeech',
+ icon: 'bullhorn',
+ }),
+ f.create('Category', {
+ id: 'cat15',
+ name: 'Consumption & Sustainability',
+ slug: 'consumption-sustainability',
+ icon: 'shopping-cart',
+ }),
+ f.create('Category', {
+ id: 'cat16',
+ name: 'Global Peace & Nonviolence',
+ slug: 'globalpeace-nonviolence',
+ icon: 'angellist',
+ }),
+ ])
+
+ await Promise.all([
+ f.create('Tag', { id: 't1', name: 'Umwelt' }),
+ f.create('Tag', { id: 't2', name: 'Naturschutz' }),
+ f.create('Tag', { id: 't3', name: 'Demokratie' }),
+ f.create('Tag', { id: 't4', name: 'Freiheit' }),
+ ])
+
+ const mention1 = 'Hey @jenny-rostock, what\'s up?'
+ const mention2 =
+ 'Hey @jenny-rostock, here is another notification for you!'
+
+ await Promise.all([
+ asAdmin.create('Post', { id: 'p0', image: faker.image.unsplash.food() }),
+ asModerator.create('Post', { id: 'p1', image: faker.image.unsplash.technology() }),
+ asUser.create('Post', { id: 'p2' }),
+ asTick.create('Post', { id: 'p3' }),
+ asTrick.create('Post', { id: 'p4' }),
+ asTrack.create('Post', { id: 'p5' }),
+ asAdmin.create('Post', { id: 'p6', image: faker.image.unsplash.buildings() }),
+ asModerator.create('Post', { id: 'p7', content: `${mention1} ${faker.lorem.paragraph()}` }),
+ asUser.create('Post', { id: 'p8', image: faker.image.unsplash.nature() }),
+ asTick.create('Post', { id: 'p9' }),
+ asTrick.create('Post', { id: 'p10' }),
+ asTrack.create('Post', { id: 'p11', image: faker.image.unsplash.people() }),
+ asAdmin.create('Post', { id: 'p12', content: `${mention2} ${faker.lorem.paragraph()}` }),
+ asModerator.create('Post', { id: 'p13' }),
+ asUser.create('Post', { id: 'p14', image: faker.image.unsplash.objects() }),
+ asTick.create('Post', { id: 'p15' }),
+ ])
+
+ await Promise.all([
+ f.relate('Post', 'Categories', { from: 'p0', to: 'cat16' }),
+ f.relate('Post', 'Categories', { from: 'p1', to: 'cat1' }),
+ f.relate('Post', 'Categories', { from: 'p2', to: 'cat2' }),
+ f.relate('Post', 'Categories', { from: 'p3', to: 'cat3' }),
+ f.relate('Post', 'Categories', { from: 'p4', to: 'cat4' }),
+ f.relate('Post', 'Categories', { from: 'p5', to: 'cat5' }),
+ f.relate('Post', 'Categories', { from: 'p6', to: 'cat6' }),
+ f.relate('Post', 'Categories', { from: 'p7', to: 'cat7' }),
+ f.relate('Post', 'Categories', { from: 'p8', to: 'cat8' }),
+ f.relate('Post', 'Categories', { from: 'p9', to: 'cat9' }),
+ f.relate('Post', 'Categories', { from: 'p10', to: 'cat10' }),
+ f.relate('Post', 'Categories', { from: 'p11', to: 'cat11' }),
+ f.relate('Post', 'Categories', { from: 'p12', to: 'cat12' }),
+ f.relate('Post', 'Categories', { from: 'p13', to: 'cat13' }),
+ f.relate('Post', 'Categories', { from: 'p14', to: 'cat14' }),
+ f.relate('Post', 'Categories', { from: 'p15', to: 'cat15' }),
+
+ f.relate('Post', 'Tags', { from: 'p0', to: 't4' }),
+ f.relate('Post', 'Tags', { from: 'p1', to: 't1' }),
+ f.relate('Post', 'Tags', { from: 'p2', to: 't2' }),
+ f.relate('Post', 'Tags', { from: 'p3', to: 't3' }),
+ f.relate('Post', 'Tags', { from: 'p4', to: 't4' }),
+ f.relate('Post', 'Tags', { from: 'p5', to: 't1' }),
+ f.relate('Post', 'Tags', { from: 'p6', to: 't2' }),
+ f.relate('Post', 'Tags', { from: 'p7', to: 't3' }),
+ f.relate('Post', 'Tags', { from: 'p8', to: 't4' }),
+ f.relate('Post', 'Tags', { from: 'p9', to: 't1' }),
+ f.relate('Post', 'Tags', { from: 'p10', to: 't2' }),
+ f.relate('Post', 'Tags', { from: 'p11', to: 't3' }),
+ f.relate('Post', 'Tags', { from: 'p12', to: 't4' }),
+ f.relate('Post', 'Tags', { from: 'p13', to: 't1' }),
+ f.relate('Post', 'Tags', { from: 'p14', to: 't2' }),
+ f.relate('Post', 'Tags', { from: 'p15', to: 't3' }),
+ ])
+
+ await Promise.all([
+ asAdmin.shout({ id: 'p2', type: 'Post' }),
+ asAdmin.shout({ id: 'p6', type: 'Post' }),
+ asModerator.shout({ id: 'p0', type: 'Post' }),
+ asModerator.shout({ id: 'p6', type: 'Post' }),
+ asUser.shout({ id: 'p6', type: 'Post' }),
+ asUser.shout({ id: 'p7', type: 'Post' }),
+ asTick.shout({ id: 'p8', type: 'Post' }),
+ asTick.shout({ id: 'p9', type: 'Post' }),
+ asTrack.shout({ id: 'p10', type: 'Post' }),
+ ])
+ await Promise.all([
+ asAdmin.shout({ id: 'p2', type: 'Post' }),
+ asAdmin.shout({ id: 'p6', type: 'Post' }),
+ asModerator.shout({ id: 'p0', type: 'Post' }),
+ asModerator.shout({ id: 'p6', type: 'Post' }),
+ asUser.shout({ id: 'p6', type: 'Post' }),
+ asUser.shout({ id: 'p7', type: 'Post' }),
+ asTick.shout({ id: 'p8', type: 'Post' }),
+ asTick.shout({ id: 'p9', type: 'Post' }),
+ asTrack.shout({ id: 'p10', type: 'Post' }),
+ ])
+
+ await Promise.all([
+ asUser.create('Comment', { id: 'c1', postId: 'p1' }),
+ asTick.create('Comment', { id: 'c2', postId: 'p1' }),
+ asTrack.create('Comment', { id: 'c3', postId: 'p3' }),
+ asTrick.create('Comment', { id: 'c4', postId: 'p2' }),
+ asModerator.create('Comment', { id: 'c5', postId: 'p3' }),
+ asAdmin.create('Comment', { id: 'c6', postId: 'p4' }),
+ asUser.create('Comment', { id: 'c7', postId: 'p2' }),
+ asTick.create('Comment', { id: 'c8', postId: 'p15' }),
+ asTrick.create('Comment', { id: 'c9', postId: 'p15' }),
+ asTrack.create('Comment', { id: 'c10', postId: 'p15' }),
+ asUser.create('Comment', { id: 'c11', postId: 'p15' }),
+ asUser.create('Comment', { id: 'c12', postId: 'p15' }),
+ ])
+
+ const disableMutation = 'mutation($id: ID!) { disable(id: $id) }'
+ await Promise.all([
+ asModerator.mutate(disableMutation, { id: 'p11' }),
+ asModerator.mutate(disableMutation, { id: 'c5' }),
+ ])
+
+ await Promise.all([
+ asTick.create('Report', { description: "I don't like this comment", id: 'c1' }),
+ asTrick.create('Report', { description: "I don't like this post", id: 'p1' }),
+ asTrack.create('Report', { description: "I don't like this user", id: 'u1' }),
+ ])
+
+ await Promise.all([
+ f.create('Organization', {
+ id: 'o1',
+ name: 'Democracy Deutschland',
+ description: 'Description for democracy-deutschland.',
+ }),
+ f.create('Organization', {
+ id: 'o2',
+ name: 'Human-Connection',
+ description: 'Description for human-connection.',
+ }),
+ f.create('Organization', {
+ id: 'o3',
+ name: 'Pro Veg',
+ description: 'Description for pro-veg.',
+ }),
+ f.create('Organization', {
+ id: 'o4',
+ name: 'Greenpeace',
+ description: 'Description for greenpeace.',
+ }),
+ ])
+
+ await Promise.all([
+ f.relate('Organization', 'CreatedBy', { from: 'u1', to: 'o1' }),
+ f.relate('Organization', 'CreatedBy', { from: 'u1', to: 'o2' }),
+ f.relate('Organization', 'OwnedBy', { from: 'u2', to: 'o2' }),
+ f.relate('Organization', 'OwnedBy', { from: 'u2', to: 'o3' }),
+ ])
+ /* eslint-disable-next-line no-console */
+ console.log('Seeded Data...')
+ } catch (err) {
+ /* eslint-disable-next-line no-console */
+ console.error(err)
+ process.exit(1)
+ }
+})()
+/* eslint-enable no-multi-spaces */
diff --git a/Human-Connection/backend/src/seed/seed-helpers.js b/Human-Connection/backend/src/seed/seed-helpers.js
new file mode 100644
index 000000000..399d06670
--- /dev/null
+++ b/Human-Connection/backend/src/seed/seed-helpers.js
@@ -0,0 +1,134 @@
+const _ = require('lodash')
+const faker = require('faker')
+const unsplashTopics = [
+ 'love',
+ 'family',
+ 'spring',
+ 'business',
+ 'nature',
+ 'travel',
+ 'happy',
+ 'landscape',
+ 'health',
+ 'friends',
+ 'computer',
+ 'autumn',
+ 'space',
+ 'animal',
+ 'smile',
+ 'face',
+ 'people',
+ 'portrait',
+ 'amazing',
+]
+let unsplashTopicsTmp = []
+
+const ngoLogos = [
+ 'http://www.fetchlogos.com/wp-content/uploads/2015/11/Girl-Scouts-Of-The-Usa-Logo.jpg',
+ 'http://logos.textgiraffe.com/logos/logo-name/Ngo-designstyle-friday-m.png',
+ 'http://seeklogo.com/images/N/ngo-logo-BD53A3E024-seeklogo.com.png',
+ 'https://dcassetcdn.com/design_img/10133/25833/25833_303600_10133_image.jpg',
+ 'https://cdn.tutsplus.com/vector/uploads/legacy/articles/08bad_ngologos/20.jpg',
+ 'https://cdn.tutsplus.com/vector/uploads/legacy/articles/08bad_ngologos/33.jpg',
+ null,
+]
+
+const difficulties = ['easy', 'medium', 'hard']
+
+export default {
+ randomItem: (items, filter) => {
+ let ids = filter
+ ? Object.keys(items).filter(id => {
+ return filter(items[id])
+ })
+ : _.keys(items)
+ let randomIds = _.shuffle(ids)
+ return items[randomIds.pop()]
+ },
+ randomItems: (items, key = 'id', min = 1, max = 1) => {
+ let randomIds = _.shuffle(_.keys(items))
+ let res = []
+
+ const count = _.random(min, max)
+
+ for (let i = 0; i < count; i++) {
+ let r = items[randomIds.pop()][key]
+ if (key === 'id') {
+ r = r.toString()
+ }
+ res.push(r)
+ }
+ return res
+ },
+ random: items => {
+ return _.shuffle(items).pop()
+ },
+ randomDifficulty: () => {
+ return _.shuffle(difficulties).pop()
+ },
+ randomLogo: () => {
+ return _.shuffle(ngoLogos).pop()
+ },
+ randomUnsplashUrl: () => {
+ if (Math.random() < 0.6) {
+ // do not attach images in 60 percent of the cases (faster seeding)
+ return
+ }
+ if (unsplashTopicsTmp.length < 2) {
+ unsplashTopicsTmp = _.shuffle(unsplashTopics)
+ }
+ return (
+ 'https://source.unsplash.com/daily?' + unsplashTopicsTmp.pop() + ',' + unsplashTopicsTmp.pop()
+ )
+ },
+ randomCategories: (seederstore, allowEmpty = false) => {
+ let count = Math.round(Math.random() * 3)
+ if (allowEmpty === false && count === 0) {
+ count = 1
+ }
+ let categorieIds = _.shuffle(_.keys(seederstore.categories))
+ let ids = []
+ for (let i = 0; i < count; i++) {
+ ids.push(categorieIds.pop())
+ }
+ return ids
+ },
+ randomAddresses: () => {
+ const count = Math.round(Math.random() * 3)
+ let addresses = []
+ for (let i = 0; i < count; i++) {
+ addresses.push({
+ city: faker.address.city(),
+ zipCode: faker.address.zipCode(),
+ street: faker.address.streetAddress(),
+ country: faker.address.countryCode(),
+ lat: 54.032726 - Math.random() * 10,
+ lng: 6.558838 + Math.random() * 10,
+ })
+ }
+ return addresses
+ },
+ /**
+ * Get array of ids from the given seederstore items after mapping them by the key in the values
+ *
+ * @param items items from the seederstore
+ * @param values values for which you need the ids
+ * @param key the field key that is represented in the values (slug, name, etc.)
+ */
+ mapIdsByKey: (items, values, key) => {
+ let res = []
+ values.forEach(value => {
+ res.push(_.find(items, [key, value]).id.toString())
+ })
+ return res
+ },
+ genInviteCode: () => {
+ const chars = '23456789abcdefghkmnpqrstuvwxyzABCDEFGHJKLMNPRSTUVWXYZ'
+ let code = ''
+ for (let i = 0; i < 8; i++) {
+ const n = _.random(0, chars.length - 1)
+ code += chars.substr(n, 1)
+ }
+ return code
+ },
+}
diff --git a/Human-Connection/backend/src/server.js b/Human-Connection/backend/src/server.js
new file mode 100644
index 000000000..7692f0d2c
--- /dev/null
+++ b/Human-Connection/backend/src/server.js
@@ -0,0 +1,47 @@
+import express from 'express'
+import helmet from 'helmet'
+import { GraphQLServer } from 'graphql-yoga'
+import CONFIG, { requiredConfigs } from './config'
+import mocks from './mocks'
+import middleware from './middleware'
+import { getDriver } from './bootstrap/neo4j'
+import decode from './jwt/decode'
+import schema from './schema'
+
+// check required configs and throw error
+// TODO check this directly in config file - currently not possible due to testsetup
+Object.entries(requiredConfigs).map(entry => {
+ if (!entry[1]) {
+ throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
+ }
+})
+
+const driver = getDriver()
+
+const createServer = options => {
+ const defaults = {
+ context: async ({ request }) => {
+ const user = await decode(driver, request.headers.authorization)
+ return {
+ driver,
+ user,
+ req: request,
+ cypherParams: {
+ currentUserId: user ? user.id : null,
+ },
+ }
+ },
+ schema,
+ debug: CONFIG.DEBUG,
+ tracing: CONFIG.DEBUG,
+ middlewares: middleware(schema),
+ mocks: CONFIG.MOCKS ? mocks : false,
+ }
+ const server = new GraphQLServer(Object.assign({}, defaults, options))
+
+ server.express.use(helmet())
+ server.express.use(express.static('public'))
+ return server
+}
+
+export default createServer
diff --git a/Human-Connection/backend/test/features/activity-delete.feature b/Human-Connection/backend/test/features/activity-delete.feature
new file mode 100644
index 000000000..76c734952
--- /dev/null
+++ b/Human-Connection/backend/test/features/activity-delete.feature
@@ -0,0 +1,55 @@
+Feature: Delete an object
+ I want to delete objects
+
+ Background:
+ Given our own server runs at "http://localhost:4123"
+ And we have the following users in our database:
+ | Slug |
+ | bernd-das-brot|
+ And I send a POST request with the following activity to "/activitypub/users/bernd-das-brot/inbox":
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://aronda.org/users/bernd-das-brot/status/lka7dfzkjn2398hsfd",
+ "type": "Create",
+ "actor": "https://aronda.org/users/bernd-das-brot",
+ "object": {
+ "id": "https://aronda.org/users/bernd-das-brot/status/kljsdfg9843jknsdf234",
+ "type": "Article",
+ "published": "2019-02-07T19:37:55.002Z",
+ "attributedTo": "https://aronda.org/users/bernd-das-brot",
+ "content": "Hi Max, how are you?",
+ "to": "https://www.w3.org/ns/activitystreams#Public"
+ }
+ }
+ """
+
+ Scenario: Deleting a post (Article Object)
+ When I send a POST request with the following activity to "/activitypub/users/bernd-das-brot/inbox":
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:4123/activitypub/users/karl-heinz/status/a4DJ2afdg323v32641vna42lkj685kasd2",
+ "type": "Delete",
+ "object": {
+ "id": "https://aronda.org/activitypub/users/bernd-das-brot/status/kljsdfg9843jknsdf234",
+ "type": "Article",
+ "published": "2019-02-07T19:37:55.002Z",
+ "attributedTo": "https://aronda.org/activitypub/users/bernd-das-brot",
+ "content": "Hi Max, how are you?",
+ "to": "https://www.w3.org/ns/activitystreams#Public"
+ }
+ }
+ """
+ Then I expect the status code to be 200
+ And the object is removed from the outbox collection of "bernd-das-brot"
+ """
+ {
+ "id": "https://aronda.org/activitypub/users/bernd-das-brot/status/kljsdfg9843jknsdf234",
+ "type": "Article",
+ "published": "2019-02-07T19:37:55.002Z",
+ "attributedTo": "https://aronda.org/activitypub/users/bernd-das-brot",
+ "content": "Hi Max, how are you?",
+ "to": "https://www.w3.org/ns/activitystreams#Public"
+ }
+ """
diff --git a/Human-Connection/backend/test/features/activity-follow.feature b/Human-Connection/backend/test/features/activity-follow.feature
new file mode 100644
index 000000000..7aa0c447d
--- /dev/null
+++ b/Human-Connection/backend/test/features/activity-follow.feature
@@ -0,0 +1,51 @@
+Feature: Follow a user
+ I want to be able to follow a user on another instance.
+ Also if I do not want to follow a previous followed user anymore,
+ I want to undo the follow.
+
+ Background:
+ Given our own server runs at "http://localhost:4123"
+ And we have the following users in our database:
+ | Slug |
+ | stuart-little |
+ | tero-vota |
+
+ @wip
+ Scenario: Send a follow to a user inbox and make sure it's added to the right followers collection
+ When I send a POST request with the following activity to "/activitypub/users/tero-vota/inbox":
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83",
+ "type": "Follow",
+ "actor": "http://localhost:4123/activitypub/users/stuart-little",
+ "object": "http://localhost:4123/activitypub/users/tero-vota"
+ }
+ """
+ Then I expect the status code to be 200
+ And the follower is added to the followers collection of "tero-vota"
+ """
+ http://localhost:4123/activitypub/users/stuart-little
+ """
+
+ Scenario: Send an undo activity to revert the previous follow activity
+ When I send a POST request with the following activity to "/activitypub/users/stuart-little/inbox":
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:4123/activitypub/users/tero-vota/status/a4DJ2afdg323v32641vna42lkj685kasd2",
+ "type": "Undo",
+ "actor": "http://localhost:4123/activitypub/users/tero-vota",
+ "object": {
+ "id": "http://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83",
+ "type": "Follow",
+ "actor": "http://localhost:4123/activitypub/users/stuart-little",
+ "object": "http://localhost:4123/activitypub/users/tero-vota"
+ }
+ }
+ """
+ Then I expect the status code to be 200
+ And the follower is removed from the followers collection of "tero-vota"
+ """
+ http://localhost:4123/activitypub/users/stuart-little
+ """
diff --git a/Human-Connection/backend/test/features/activity-like.feature b/Human-Connection/backend/test/features/activity-like.feature
new file mode 100644
index 000000000..26ef9c857
--- /dev/null
+++ b/Human-Connection/backend/test/features/activity-like.feature
@@ -0,0 +1,43 @@
+Feature: Like an object like an article or note
+ As a user I want to like others posts
+ Also if I do not want to follow a previous followed user anymore,
+ I want to undo the follow.
+
+ Background:
+ Given our own server runs at "http://localhost:4123"
+ And we have the following users in our database:
+ | Slug |
+ | karl-heinz |
+ | peter-lustiger |
+ And I send a POST request with the following activity to "/activitypub/users/bernd-das-brot/inbox":
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:4123/activitypub/users/karl-heinz/status/faslkasa7dasfzkjn2398hsfd",
+ "type": "Create",
+ "actor": "http://localhost:4123/activitypub/users/karl-heinz",
+ "object": {
+ "id": "http://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf",
+ "type": "Article",
+ "published": "2019-02-07T19:37:55.002Z",
+ "attributedTo": "http://localhost:4123/activitypub/users/karl-heinz",
+ "content": "Hi Max, how are you?",
+ "to": "https://www.w3.org/ns/activitystreams#Public"
+ }
+ }
+ """
+
+ @wip
+ Scenario: Send a like of a person to an users inbox and make sure it's added to the likes collection
+ When I send a POST request with the following activity to "/activitypub/users/karl-heinz/inbox":
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:4123/activitypub/users/peter-lustiger/status/83J23549sda1k72fsa4567na42312455kad83",
+ "type": "Like",
+ "actor": "http://localhost:4123/activitypub/users/peter-lustiger",
+ "object": "http://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf"
+ }
+ """
+ Then I expect the status code to be 200
+ And the post with id "dkasfljsdfaafg9843jknsdf" has been liked by "peter-lustiger"
diff --git a/Human-Connection/backend/test/features/collection.feature b/Human-Connection/backend/test/features/collection.feature
new file mode 100644
index 000000000..1bb4737e0
--- /dev/null
+++ b/Human-Connection/backend/test/features/collection.feature
@@ -0,0 +1,101 @@
+Feature: Receiving collections
+ As a member of the Fediverse I want to be able of fetching collections
+
+ Background:
+ Given our own server runs at "http://localhost:4123"
+ And we have the following users in our database:
+ | Slug |
+ | renate-oberdorfer |
+
+ Scenario: Send a request to the outbox URI of peter-lustig and expect a ordered collection
+ When I send a GET request to "/activitypub/users/renate-oberdorfer/outbox"
+ Then I expect the status code to be 200
+ And I receive the following json:
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox",
+ "summary": "renate-oberdorfers outbox collection",
+ "type": "OrderedCollection",
+ "first": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true",
+ "totalItems": 0
+ }
+ """
+
+ Scenario: Send a request to the following URI of peter-lustig and expect a ordered collection
+ When I send a GET request to "/activitypub/users/renate-oberdorfer/following"
+ Then I expect the status code to be 200
+ And I receive the following json:
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/following",
+ "summary": "renate-oberdorfers following collection",
+ "type": "OrderedCollection",
+ "first": "http://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true",
+ "totalItems": 0
+ }
+ """
+
+ Scenario: Send a request to the followers URI of peter-lustig and expect a ordered collection
+ When I send a GET request to "/activitypub/users/renate-oberdorfer/followers"
+ Then I expect the status code to be 200
+ And I receive the following json:
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers",
+ "summary": "renate-oberdorfers followers collection",
+ "type": "OrderedCollection",
+ "first": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true",
+ "totalItems": 0
+ }
+ """
+
+ Scenario: Send a request to the outbox URI of peter-lustig and expect a paginated outbox collection
+ When I send a GET request to "/activitypub/users/renate-oberdorfer/outbox?page=true"
+ Then I expect the status code to be 200
+ And I receive the following json:
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true",
+ "summary": "renate-oberdorfers outbox collection",
+ "type": "OrderedCollectionPage",
+ "totalItems": 0,
+ "partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/outbox",
+ "orderedItems": []
+ }
+ """
+
+ Scenario: Send a request to the following URI of peter-lustig and expect a paginated following collection
+ When I send a GET request to "/activitypub/users/renate-oberdorfer/following?page=true"
+ Then I expect the status code to be 200
+ And I receive the following json:
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true",
+ "summary": "renate-oberdorfers following collection",
+ "type": "OrderedCollectionPage",
+ "totalItems": 0,
+ "partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/following",
+ "orderedItems": []
+ }
+ """
+
+ Scenario: Send a request to the followers URI of peter-lustig and expect a paginated followers collection
+ When I send a GET request to "/activitypub/users/renate-oberdorfer/followers?page=true"
+ Then I expect the status code to be 200
+ And I receive the following json:
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true",
+ "summary": "renate-oberdorfers followers collection",
+ "type": "OrderedCollectionPage",
+ "totalItems": 0,
+ "partOf": "http://localhost:4123/activitypub/users/renate-oberdorfer/followers",
+ "orderedItems": []
+ }
+ """
diff --git a/Human-Connection/backend/test/features/object-article.feature b/Human-Connection/backend/test/features/object-article.feature
new file mode 100644
index 000000000..030e408e9
--- /dev/null
+++ b/Human-Connection/backend/test/features/object-article.feature
@@ -0,0 +1,30 @@
+Feature: Send and receive Articles
+ I want to send and receive article's via ActivityPub
+
+ Background:
+ Given our own server runs at "http://localhost:4123"
+ And we have the following users in our database:
+ | Slug |
+ | marvin |
+ | max |
+
+ Scenario: Send an article to a user inbox and make sure it's added to the inbox
+ When I send a POST request with the following activity to "/activitypub/users/max/inbox":
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://aronda.org/users/marvin/status/lka7dfzkjn2398hsfd",
+ "type": "Create",
+ "actor": "https://aronda.org/users/marvin",
+ "object": {
+ "id": "https://aronda.org/users/marvin/status/kljsdfg9843jknsdf",
+ "type": "Article",
+ "published": "2019-02-07T19:37:55.002Z",
+ "attributedTo": "https://aronda.org/users/marvin",
+ "content": "Hi Max, how are you?",
+ "to": "as:Public"
+ }
+ }
+ """
+ Then I expect the status code to be 200
+ And the post with id "kljsdfg9843jknsdf" to be created
diff --git a/Human-Connection/backend/test/features/support/steps.js b/Human-Connection/backend/test/features/support/steps.js
new file mode 100644
index 000000000..fdbca289c
--- /dev/null
+++ b/Human-Connection/backend/test/features/support/steps.js
@@ -0,0 +1,158 @@
+// features/support/steps.js
+import { Given, When, Then, AfterAll } from 'cucumber'
+import { expect } from 'chai'
+// import { client } from '../../../src/activitypub/apollo-client'
+import { GraphQLClient } from 'graphql-request'
+import Factory from '../../../src/seed/factories'
+import { host } from '../../../src/jest/helpers'
+const debug = require('debug')('ea:test:steps')
+
+const factory = Factory()
+const client = new GraphQLClient(host)
+
+function createUser (slug) {
+ debug(`creating user ${slug}`)
+ return factory.create('User', {
+ name: slug,
+ email: 'example@test.org',
+ password: '1234'
+ })
+ // await login({ email: 'example@test.org', password: '1234' })
+}
+
+Given('our own server runs at {string}', function (string) {
+ // just documenation
+})
+
+Given('we have the following users in our database:', function (dataTable) {
+ return Promise.all(dataTable.hashes().map((user) => {
+ return createUser(user.Slug)
+ }))
+})
+
+When('I send a GET request to {string}', async function (pathname) {
+ const response = await this.get(pathname)
+ this.lastContentType = response.lastContentType
+
+ this.lastResponses.push(response.lastResponse)
+ this.statusCode = response.statusCode
+})
+
+When('I send a POST request with the following activity to {string}:', async function (inboxUrl, activity) {
+ debug(`inboxUrl = ${inboxUrl}`)
+ debug(`activity = ${activity}`)
+ const splitted = inboxUrl.split('/')
+ const slug = splitted[splitted.indexOf('users') + 1]
+ let result
+ do {
+ result = await client.request(`
+ query {
+ User(slug: "${slug}") {
+ id
+ slug
+ actorId
+ }
+ }
+ `)
+ } while (result.User.length === 0)
+ this.lastInboxUrl = inboxUrl
+ this.lastActivity = activity
+ const response = await this.post(inboxUrl, activity)
+
+ this.lastResponses.push(response.lastResponse)
+ this.lastResponse = response.lastResponse
+ this.statusCode = response.statusCode
+})
+
+Then('I receive the following json:', function (docString) {
+ const parsedDocString = JSON.parse(docString)
+ const parsedLastResponse = JSON.parse(this.lastResponses.shift())
+ if (Array.isArray(parsedDocString.orderedItems)) {
+ parsedDocString.orderedItems.forEach((el) => {
+ delete el.id
+ if (el.object) delete el.object.published
+ })
+ parsedLastResponse.orderedItems.forEach((el) => {
+ delete el.id
+ if (el.object) delete el.object.published
+ })
+ }
+ if (parsedDocString.publicKey && parsedDocString.publicKey.publicKeyPem) {
+ delete parsedDocString.publicKey.publicKeyPem
+ delete parsedLastResponse.publicKey.publicKeyPem
+ }
+ expect(parsedDocString).to.eql(parsedLastResponse)
+})
+
+Then('I expect the Content-Type to be {string}', function (contentType) {
+ expect(this.lastContentType).to.equal(contentType)
+})
+
+Then('I expect the status code to be {int}', function (statusCode) {
+ expect(this.statusCode).to.equal(statusCode)
+})
+
+Then('the activity is added to the {string} collection', async function (collectionName) {
+ const response = await this.get(this.lastInboxUrl.replace('inbox', collectionName) + '?page=true')
+ debug(`orderedItems = ${JSON.parse(response.lastResponse).orderedItems}`)
+ expect(JSON.parse(response.lastResponse).orderedItems).to.include(JSON.parse(this.lastActivity).object)
+})
+
+Then('the follower is added to the followers collection of {string}', async function (userName, follower) {
+ const response = await this.get(`/activitypub/users/${userName}/followers?page=true`)
+ const responseObject = JSON.parse(response.lastResponse)
+ expect(responseObject.orderedItems).to.include(follower)
+})
+
+Then('the follower is removed from the followers collection of {string}', async function (userName, follower) {
+ const response = await this.get(`/activitypub/users/${userName}/followers?page=true`)
+ const responseObject = JSON.parse(response.lastResponse)
+ expect(responseObject.orderedItems).to.not.include(follower)
+})
+
+Then('the post with id {string} to be created', async function (id) {
+ let result
+ do {
+ result = await client.request(`
+ query {
+ Post(id: "${id}") {
+ title
+ }
+ }
+ `)
+ } while (result.Post.length === 0)
+
+ expect(result.Post).to.be.an('array').that.is.not.empty // eslint-disable-line
+})
+
+Then('the object is removed from the outbox collection of {string}', async function (name, object) {
+ const response = await this.get(`/activitypub/users/${name}/outbox?page=true`)
+ const parsedResponse = JSON.parse(response.lastResponse)
+ expect(parsedResponse.orderedItems).to.not.include(object)
+})
+
+Then('I send a GET request to {string} and expect a ordered collection', () => {
+
+})
+
+Then('the activity is added to the users inbox collection', async function () {
+
+})
+
+Then('the post with id {string} has been liked by {string}', async function (id, slug) {
+ let result
+ do {
+ result = await client.request(`
+ query {
+ Post(id: "${id}") {
+ shoutedBy {
+ slug
+ }
+ }
+ }
+ `)
+ } while (result.Post.length === 0)
+
+ expect(result.Post[0].shoutedBy).to.be.an('array').that.is.not.empty // eslint-disable-line
+ expect(result.Post[0].shoutedBy[0].slug).to.equal(slug)
+})
diff --git a/Human-Connection/backend/test/features/webfinger.feature b/Human-Connection/backend/test/features/webfinger.feature
new file mode 100644
index 000000000..72062839a
--- /dev/null
+++ b/Human-Connection/backend/test/features/webfinger.feature
@@ -0,0 +1,65 @@
+Feature: Webfinger discovery
+ From an external server, e.g. Mastodon
+ I want to search for an actor alias
+ In order to follow the actor
+
+ Background:
+ Given our own server runs at "http://localhost:4123"
+ And we have the following users in our database:
+ | Slug |
+ | peter-lustiger |
+
+ Scenario: Search
+ When I send a GET request to "/.well-known/webfinger?resource=acct:peter-lustiger@localhost"
+ Then I receive the following json:
+ """
+ {
+ "subject": "acct:peter-lustiger@localhost:4123",
+ "links": [
+ {
+ "rel": "self",
+ "type": "application/activity+json",
+ "href": "http://localhost:4123/activitypub/users/peter-lustiger"
+ }
+ ]
+ }
+ """
+ And I expect the Content-Type to be "application/jrd+json; charset=utf-8"
+
+ Scenario: User does not exist
+ When I send a GET request to "/.well-known/webfinger?resource=acct:nonexisting@localhost"
+ Then I receive the following json:
+ """
+ {
+ "error": "No record found for nonexisting@localhost."
+ }
+ """
+
+ Scenario: Receiving an actor object
+ When I send a GET request to "/activitypub/users/peter-lustiger"
+ Then I receive the following json:
+ """
+ {
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1"
+ ],
+ "id": "http://localhost:4123/activitypub/users/peter-lustiger",
+ "type": "Person",
+ "preferredUsername": "peter-lustiger",
+ "name": "peter-lustiger",
+ "following": "http://localhost:4123/activitypub/users/peter-lustiger/following",
+ "followers": "http://localhost:4123/activitypub/users/peter-lustiger/followers",
+ "inbox": "http://localhost:4123/activitypub/users/peter-lustiger/inbox",
+ "outbox": "http://localhost:4123/activitypub/users/peter-lustiger/outbox",
+ "url": "http://localhost:4123/activitypub/@peter-lustiger",
+ "endpoints": {
+ "sharedInbox": "http://localhost:4123/activitypub/inbox"
+ },
+ "publicKey": {
+ "id": "http://localhost:4123/activitypub/users/peter-lustiger#main-key",
+ "owner": "http://localhost:4123/activitypub/users/peter-lustiger",
+ "publicKeyPem": "adglkjlk89235kjn8obn2384f89z5bv9..."
+ }
+ }
+ """
diff --git a/Human-Connection/backend/test/features/world.js b/Human-Connection/backend/test/features/world.js
new file mode 100644
index 000000000..be436b536
--- /dev/null
+++ b/Human-Connection/backend/test/features/world.js
@@ -0,0 +1,58 @@
+// features/support/world.js
+import { setWorldConstructor } from 'cucumber'
+import request from 'request'
+const debug = require('debug')('ea:test:world')
+
+class CustomWorld {
+ constructor () {
+ // webFinger.feature
+ this.lastResponses = []
+ this.lastContentType = null
+ this.lastInboxUrl = null
+ this.lastActivity = null
+ // object-article.feature
+ this.statusCode = null
+ }
+ get (pathname) {
+ return new Promise((resolve, reject) => {
+ request(`http://localhost:4123/${this.replaceSlashes(pathname)}`, {
+ headers: {
+ 'Accept': 'application/activity+json'
+ }}, function (error, response, body) {
+ if (!error) {
+ debug(`get content-type = ${response.headers['content-type']}`)
+ debug(`get body = ${JSON.stringify(typeof body === 'string' ? JSON.parse(body) : body, null, 2)}`)
+ resolve({ lastResponse: body, lastContentType: response.headers['content-type'], statusCode: response.statusCode })
+ } else {
+ reject(error)
+ }
+ })
+ })
+ }
+
+ replaceSlashes (pathname) {
+ return pathname.replace(/^\/+/, '')
+ }
+
+ post (pathname, activity) {
+ return new Promise((resolve, reject) => {
+ request({
+ url: `http://localhost:4123/${this.replaceSlashes(pathname)}`,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/activity+json'
+ },
+ body: activity
+ }, function (error, response, body) {
+ if (!error) {
+ debug(`post response = ${response.headers['content-type']}`)
+ resolve({ lastResponse: body, lastContentType: response.headers['content-type'], statusCode: response.statusCode })
+ } else {
+ reject(error)
+ }
+ })
+ })
+ }
+}
+
+setWorldConstructor(CustomWorld)
diff --git a/Human-Connection/backend/testing.md b/Human-Connection/backend/testing.md
new file mode 100644
index 000000000..600973450
--- /dev/null
+++ b/Human-Connection/backend/testing.md
@@ -0,0 +1,2 @@
+# Unit Testing
+
diff --git a/Human-Connection/backend/yarn.lock b/Human-Connection/backend/yarn.lock
new file mode 100644
index 000000000..53075537f
--- /dev/null
+++ b/Human-Connection/backend/yarn.lock
@@ -0,0 +1,8142 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@apollographql/apollo-tools@^0.3.6":
+ version "0.3.7"
+ resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.3.7.tgz#3bc9c35b9fff65febd4ddc0c1fc04677693a3d40"
+ integrity sha512-+ertvzAwzkYmuUtT8zH3Zi6jPdyxZwOgnYaZHY7iLnMVJDhQKWlkyjLMF8wyzlPiEdDImVUMm5lOIBZo7LkGlg==
+ dependencies:
+ apollo-env "0.5.1"
+
+"@apollographql/graphql-playground-html@1.6.20":
+ version "1.6.20"
+ resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.20.tgz#bf9f2acdf319c0959fad8ec1239741dd2ead4e8d"
+ integrity sha512-3LWZa80HcP70Pl+H4KhLDJ7S0px+9/c8GTXdl6SpunRecUaB27g/oOQnAjNHLHdbWuGE0uyqcuGiTfbKB3ilaQ==
+
+"@babel/cli@~7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.4.4.tgz#5454bb7112f29026a4069d8e6f0e1794e651966c"
+ integrity sha512-XGr5YjQSjgTa6OzQZY57FAJsdeVSAKR/u/KA5exWIz66IKtv/zXtHy+fIZcMry/EgYegwuHE7vzGnrFhjdIAsQ==
+ dependencies:
+ commander "^2.8.1"
+ convert-source-map "^1.1.0"
+ fs-readdir-recursive "^1.1.0"
+ glob "^7.0.0"
+ lodash "^4.17.11"
+ mkdirp "^0.5.1"
+ output-file-sync "^2.0.0"
+ slash "^2.0.0"
+ source-map "^0.5.0"
+ optionalDependencies:
+ chokidar "^2.0.4"
+
+"@babel/code-frame@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
+ integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==
+ dependencies:
+ "@babel/highlight" "^7.0.0"
+
+"@babel/core@^7.1.0", "@babel/core@~7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a"
+ integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/generator" "^7.4.4"
+ "@babel/helpers" "^7.4.4"
+ "@babel/parser" "^7.4.5"
+ "@babel/template" "^7.4.4"
+ "@babel/traverse" "^7.4.5"
+ "@babel/types" "^7.4.4"
+ convert-source-map "^1.1.0"
+ debug "^4.1.0"
+ json5 "^2.1.0"
+ lodash "^4.17.11"
+ resolve "^1.3.2"
+ semver "^5.4.1"
+ source-map "^0.5.0"
+
+"@babel/generator@^7.0.0", "@babel/generator@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041"
+ integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==
+ dependencies:
+ "@babel/types" "^7.4.4"
+ jsesc "^2.5.1"
+ lodash "^4.17.11"
+ source-map "^0.5.0"
+ trim-right "^1.0.1"
+
+"@babel/helper-annotate-as-pure@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32"
+ integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-builder-binary-assignment-operator-visitor@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz#6b69628dfe4087798e0c4ed98e3d4a6b2fbd2f5f"
+ integrity sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==
+ dependencies:
+ "@babel/helper-explode-assignable-expression" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-call-delegate@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz#87c1f8ca19ad552a736a7a27b1c1fcf8b1ff1f43"
+ integrity sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==
+ dependencies:
+ "@babel/helper-hoist-variables" "^7.4.4"
+ "@babel/traverse" "^7.4.4"
+ "@babel/types" "^7.4.4"
+
+"@babel/helper-define-map@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz#6969d1f570b46bdc900d1eba8e5d59c48ba2c12a"
+ integrity sha512-IX3Ln8gLhZpSuqHJSnTNBWGDE9kdkTEWl21A/K7PQ00tseBwbqCHTvNLHSBd9M0R5rER4h5Rsvj9vw0R5SieBg==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/types" "^7.4.4"
+ lodash "^4.17.11"
+
+"@babel/helper-explode-assignable-expression@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz#537fa13f6f1674df745b0c00ec8fe4e99681c8f6"
+ integrity sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==
+ dependencies:
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-function-name@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53"
+ integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==
+ dependencies:
+ "@babel/helper-get-function-arity" "^7.0.0"
+ "@babel/template" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-get-function-arity@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3"
+ integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-hoist-variables@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz#0298b5f25c8c09c53102d52ac4a98f773eb2850a"
+ integrity sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==
+ dependencies:
+ "@babel/types" "^7.4.4"
+
+"@babel/helper-member-expression-to-functions@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz#8cd14b0a0df7ff00f009e7d7a436945f47c7a16f"
+ integrity sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-module-imports@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d"
+ integrity sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-module-transforms@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.1.0.tgz#470d4f9676d9fad50b324cdcce5fbabbc3da5787"
+ integrity sha512-0JZRd2yhawo79Rcm4w0LwSMILFmFXjugG3yqf+P/UsKsRS1mJCmMwwlHDlMg7Avr9LrvSpp4ZSULO9r8jpCzcw==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-simple-access" "^7.1.0"
+ "@babel/helper-split-export-declaration" "^7.0.0"
+ "@babel/template" "^7.1.0"
+ "@babel/types" "^7.0.0"
+ lodash "^4.17.10"
+
+"@babel/helper-module-transforms@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz#96115ea42a2f139e619e98ed46df6019b94414b8"
+ integrity sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-simple-access" "^7.1.0"
+ "@babel/helper-split-export-declaration" "^7.4.4"
+ "@babel/template" "^7.4.4"
+ "@babel/types" "^7.4.4"
+ lodash "^4.17.11"
+
+"@babel/helper-optimise-call-expression@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz#a2920c5702b073c15de51106200aa8cad20497d5"
+ integrity sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-plugin-utils@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250"
+ integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==
+
+"@babel/helper-regex@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.0.0.tgz#2c1718923b57f9bbe64705ffe5640ac64d9bdb27"
+ integrity sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg==
+ dependencies:
+ lodash "^4.17.10"
+
+"@babel/helper-regex@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.4.4.tgz#a47e02bc91fb259d2e6727c2a30013e3ac13c4a2"
+ integrity sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q==
+ dependencies:
+ lodash "^4.17.11"
+
+"@babel/helper-remap-async-to-generator@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f"
+ integrity sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.0.0"
+ "@babel/helper-wrap-function" "^7.1.0"
+ "@babel/template" "^7.1.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-replace-supers@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.1.0.tgz#5fc31de522ec0ef0899dc9b3e7cf6a5dd655f362"
+ integrity sha512-BvcDWYZRWVuDeXTYZWxekQNO5D4kO55aArwZOTFXw6rlLQA8ZaDicJR1sO47h+HrnCiDFiww0fSPV0d713KBGQ==
+ dependencies:
+ "@babel/helper-member-expression-to-functions" "^7.0.0"
+ "@babel/helper-optimise-call-expression" "^7.0.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-replace-supers@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz#aee41783ebe4f2d3ab3ae775e1cc6f1a90cefa27"
+ integrity sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg==
+ dependencies:
+ "@babel/helper-member-expression-to-functions" "^7.0.0"
+ "@babel/helper-optimise-call-expression" "^7.0.0"
+ "@babel/traverse" "^7.4.4"
+ "@babel/types" "^7.4.4"
+
+"@babel/helper-simple-access@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c"
+ integrity sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==
+ dependencies:
+ "@babel/template" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-split-export-declaration@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813"
+ integrity sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-split-export-declaration@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677"
+ integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==
+ dependencies:
+ "@babel/types" "^7.4.4"
+
+"@babel/helper-wrap-function@^7.1.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa"
+ integrity sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/template" "^7.1.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.2.0"
+
+"@babel/helpers@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5"
+ integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A==
+ dependencies:
+ "@babel/template" "^7.4.4"
+ "@babel/traverse" "^7.4.4"
+ "@babel/types" "^7.4.4"
+
+"@babel/highlight@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
+ integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^4.0.0"
+
+"@babel/node@~7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.4.5.tgz#bce71bb44d902bfdd4da0b9c839a8a90fc084056"
+ integrity sha512-nDXPT0KwYMycDHhFG9wKlkipCR+iXzzoX9bD2aF2UABLhQ13AKhNi5Y61W8ASGPPll/7p9GrHesmlOgTUJVcfw==
+ dependencies:
+ "@babel/polyfill" "^7.0.0"
+ "@babel/register" "^7.0.0"
+ commander "^2.8.1"
+ lodash "^4.17.11"
+ node-environment-flags "^1.0.5"
+ v8flags "^3.1.1"
+
+"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872"
+ integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==
+
+"@babel/plugin-proposal-async-generator-functions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e"
+ integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-remap-async-to-generator" "^7.1.0"
+ "@babel/plugin-syntax-async-generators" "^7.2.0"
+
+"@babel/plugin-proposal-json-strings@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
+ integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-syntax-json-strings" "^7.2.0"
+
+"@babel/plugin-proposal-object-rest-spread@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.4.tgz#1ef173fcf24b3e2df92a678f027673b55e7e3005"
+ integrity sha512-dMBG6cSPBbHeEBdFXeQ2QLc5gUpg4Vkaz8octD4aoW/ISO+jBOcsuxYL7bsb5WSu8RLP6boxrBIALEHgoHtO9g==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
+
+"@babel/plugin-proposal-optional-catch-binding@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5"
+ integrity sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
+
+"@babel/plugin-proposal-throw-expressions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.2.0.tgz#2d9e452d370f139000e51db65d0a85dc60c64739"
+ integrity sha512-adsydM8DQF4i5DLNO4ySAU5VtHTPewOtNBV3u7F4lNMPADFF9bWQ+iDtUUe8+033cYCUz+bFlQdXQJmJOwoLpw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-syntax-throw-expressions" "^7.2.0"
+
+"@babel/plugin-proposal-unicode-property-regex@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78"
+ integrity sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.4.4"
+ regexpu-core "^4.5.4"
+
+"@babel/plugin-syntax-async-generators@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f"
+ integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-json-strings@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470"
+ integrity sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e"
+ integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-optional-catch-binding@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz#a94013d6eda8908dfe6a477e7f9eda85656ecf5c"
+ integrity sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-throw-expressions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.2.0.tgz#79001ee2afe1b174b1733cdc2fc69c9a46a0f1f8"
+ integrity sha512-ngwynuqu1Rx0JUS9zxSDuPgW1K8TyVZCi2hHehrL4vyjqE7RGoNHWlZsS7KQT2vw9Yjk4YLa0+KldBXTRdPLRg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-arrow-functions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550"
+ integrity sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-async-to-generator@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.4.tgz#a3f1d01f2f21cadab20b33a82133116f14fb5894"
+ integrity sha512-YiqW2Li8TXmzgbXw+STsSqPBPFnGviiaSp6CYOq55X8GQ2SGVLrXB6pNid8HkqkZAzOH6knbai3snhP7v0fNwA==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-remap-async-to-generator" "^7.1.0"
+
+"@babel/plugin-transform-block-scoped-functions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190"
+ integrity sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-block-scoping@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.4.tgz#c13279fabf6b916661531841a23c4b7dae29646d"
+ integrity sha512-jkTUyWZcTrwxu5DD4rWz6rDB5Cjdmgz6z7M7RLXOJyCUkFBawssDGcGh8M/0FTSB87avyJI1HsTwUXp9nKA1PA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ lodash "^4.17.11"
+
+"@babel/plugin-transform-classes@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.4.tgz#0ce4094cdafd709721076d3b9c38ad31ca715eb6"
+ integrity sha512-/e44eFLImEGIpL9qPxSRat13I5QNRgBLu2hOQJCF7VLy/otSM/sypV1+XaIw5+502RX/+6YaSAPmldk+nhHDPw==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.0.0"
+ "@babel/helper-define-map" "^7.4.4"
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-optimise-call-expression" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-replace-supers" "^7.4.4"
+ "@babel/helper-split-export-declaration" "^7.4.4"
+ globals "^11.1.0"
+
+"@babel/plugin-transform-computed-properties@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz#83a7df6a658865b1c8f641d510c6f3af220216da"
+ integrity sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-destructuring@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.4.tgz#9d964717829cc9e4b601fc82a26a71a4d8faf20f"
+ integrity sha512-/aOx+nW0w8eHiEHm+BTERB2oJn5D127iye/SUQl7NjHy0lf+j7h4MKMMSOwdazGq9OxgiNADncE+SRJkCxjZpQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-dotall-regex@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3"
+ integrity sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.4.4"
+ regexpu-core "^4.5.4"
+
+"@babel/plugin-transform-duplicate-keys@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz#d952c4930f312a4dbfff18f0b2914e60c35530b3"
+ integrity sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-exponentiation-operator@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008"
+ integrity sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==
+ dependencies:
+ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-for-of@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz#0267fc735e24c808ba173866c6c4d1440fc3c556"
+ integrity sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-function-name@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz#e1436116abb0610c2259094848754ac5230922ad"
+ integrity sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-literals@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz#690353e81f9267dad4fd8cfd77eafa86aba53ea1"
+ integrity sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-member-expression-literals@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz#fa10aa5c58a2cb6afcf2c9ffa8cb4d8b3d489a2d"
+ integrity sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-modules-amd@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz#82a9bce45b95441f617a24011dc89d12da7f4ee6"
+ integrity sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-modules-commonjs@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.4.tgz#0bef4713d30f1d78c2e59b3d6db40e60192cac1e"
+ integrity sha512-4sfBOJt58sEo9a2BQXnZq+Q3ZTSAUXyK3E30o36BOGnJ+tvJ6YSxF0PG6kERvbeISgProodWuI9UVG3/FMY6iw==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.4.4"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-simple-access" "^7.1.0"
+
+"@babel/plugin-transform-modules-systemjs@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.4.tgz#dc83c5665b07d6c2a7b224c00ac63659ea36a405"
+ integrity sha512-MSiModfILQc3/oqnG7NrP1jHaSPryO6tA2kOMmAQApz5dayPxWiHqmq4sWH2xF5LcQK56LlbKByCd8Aah/OIkQ==
+ dependencies:
+ "@babel/helper-hoist-variables" "^7.4.4"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-modules-umd@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae"
+ integrity sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-named-capturing-groups-regex@^7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz#9d269fd28a370258199b4294736813a60bbdd106"
+ integrity sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg==
+ dependencies:
+ regexp-tree "^0.1.6"
+
+"@babel/plugin-transform-new-target@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz#18d120438b0cc9ee95a47f2c72bc9768fbed60a5"
+ integrity sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-object-super@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz#b35d4c10f56bab5d650047dad0f1d8e8814b6598"
+ integrity sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-replace-supers" "^7.1.0"
+
+"@babel/plugin-transform-parameters@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz#7556cf03f318bd2719fe4c922d2d808be5571e16"
+ integrity sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==
+ dependencies:
+ "@babel/helper-call-delegate" "^7.4.4"
+ "@babel/helper-get-function-arity" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-property-literals@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz#03e33f653f5b25c4eb572c98b9485055b389e905"
+ integrity sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-regenerator@^7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz#629dc82512c55cee01341fb27bdfcb210354680f"
+ integrity sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==
+ dependencies:
+ regenerator-transform "^0.14.0"
+
+"@babel/plugin-transform-reserved-words@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz#4792af87c998a49367597d07fedf02636d2e1634"
+ integrity sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-shorthand-properties@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0"
+ integrity sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-spread@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.0.tgz#0c76c12a3b5826130078ee8ec84a7a8e4afd79c4"
+ integrity sha512-7TtPIdwjS/i5ZBlNiQePQCovDh9pAhVbp/nGVRBZuUdBiVRThyyLend3OHobc0G+RLCPPAN70+z/MAMhsgJd/A==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-sticky-regex@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz#a1e454b5995560a9c1e0d537dfc15061fd2687e1"
+ integrity sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.0.0"
+
+"@babel/plugin-transform-template-literals@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz#9d28fea7bbce637fb7612a0750989d8321d4bcb0"
+ integrity sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-typeof-symbol@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz#117d2bcec2fbf64b4b59d1f9819894682d29f2b2"
+ integrity sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-unicode-regex@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz#ab4634bb4f14d36728bf5978322b35587787970f"
+ integrity sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.4.4"
+ regexpu-core "^4.5.4"
+
+"@babel/polyfill@^7.0.0", "@babel/polyfill@^7.2.3":
+ version "7.2.5"
+ resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.2.5.tgz#6c54b964f71ad27edddc567d065e57e87ed7fa7d"
+ integrity sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug==
+ dependencies:
+ core-js "^2.5.7"
+ regenerator-runtime "^0.12.0"
+
+"@babel/preset-env@~7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58"
+ integrity sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-proposal-async-generator-functions" "^7.2.0"
+ "@babel/plugin-proposal-json-strings" "^7.2.0"
+ "@babel/plugin-proposal-object-rest-spread" "^7.4.4"
+ "@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
+ "@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
+ "@babel/plugin-syntax-async-generators" "^7.2.0"
+ "@babel/plugin-syntax-json-strings" "^7.2.0"
+ "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
+ "@babel/plugin-transform-arrow-functions" "^7.2.0"
+ "@babel/plugin-transform-async-to-generator" "^7.4.4"
+ "@babel/plugin-transform-block-scoped-functions" "^7.2.0"
+ "@babel/plugin-transform-block-scoping" "^7.4.4"
+ "@babel/plugin-transform-classes" "^7.4.4"
+ "@babel/plugin-transform-computed-properties" "^7.2.0"
+ "@babel/plugin-transform-destructuring" "^7.4.4"
+ "@babel/plugin-transform-dotall-regex" "^7.4.4"
+ "@babel/plugin-transform-duplicate-keys" "^7.2.0"
+ "@babel/plugin-transform-exponentiation-operator" "^7.2.0"
+ "@babel/plugin-transform-for-of" "^7.4.4"
+ "@babel/plugin-transform-function-name" "^7.4.4"
+ "@babel/plugin-transform-literals" "^7.2.0"
+ "@babel/plugin-transform-member-expression-literals" "^7.2.0"
+ "@babel/plugin-transform-modules-amd" "^7.2.0"
+ "@babel/plugin-transform-modules-commonjs" "^7.4.4"
+ "@babel/plugin-transform-modules-systemjs" "^7.4.4"
+ "@babel/plugin-transform-modules-umd" "^7.2.0"
+ "@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5"
+ "@babel/plugin-transform-new-target" "^7.4.4"
+ "@babel/plugin-transform-object-super" "^7.2.0"
+ "@babel/plugin-transform-parameters" "^7.4.4"
+ "@babel/plugin-transform-property-literals" "^7.2.0"
+ "@babel/plugin-transform-regenerator" "^7.4.5"
+ "@babel/plugin-transform-reserved-words" "^7.2.0"
+ "@babel/plugin-transform-shorthand-properties" "^7.2.0"
+ "@babel/plugin-transform-spread" "^7.2.0"
+ "@babel/plugin-transform-sticky-regex" "^7.2.0"
+ "@babel/plugin-transform-template-literals" "^7.4.4"
+ "@babel/plugin-transform-typeof-symbol" "^7.2.0"
+ "@babel/plugin-transform-unicode-regex" "^7.4.4"
+ "@babel/types" "^7.4.4"
+ browserslist "^4.6.0"
+ core-js-compat "^3.1.1"
+ invariant "^2.2.2"
+ js-levenshtein "^1.1.3"
+ semver "^5.5.0"
+
+"@babel/register@^7.0.0", "@babel/register@~7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.4.4.tgz#370a68ba36f08f015a8b35d4864176c6b65d7a23"
+ integrity sha512-sn51H88GRa00+ZoMqCVgOphmswG4b7mhf9VOB0LUBAieykq2GnRFerlN+JQkO/ntT7wz4jaHNSRPg9IdMPEUkA==
+ dependencies:
+ core-js "^3.0.0"
+ find-cache-dir "^2.0.0"
+ lodash "^4.17.11"
+ mkdirp "^0.5.1"
+ pirates "^4.0.0"
+ source-map-support "^0.5.9"
+
+"@babel/runtime@^7.0.0":
+ version "7.4.2"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.2.tgz#f5ab6897320f16decd855eed70b705908a313fe8"
+ integrity sha512-7Bl2rALb7HpvXFL7TETNzKSAeBVCPHELzc0C//9FCxN8nsiueWSJBqaF+2oIJScyILStASR/Cx5WMkXGYTiJFA==
+ dependencies:
+ regenerator-runtime "^0.13.2"
+
+"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237"
+ integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/parser" "^7.4.4"
+ "@babel/types" "^7.4.4"
+
+"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216"
+ integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/generator" "^7.4.4"
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-split-export-declaration" "^7.4.4"
+ "@babel/parser" "^7.4.5"
+ "@babel/types" "^7.4.4"
+ debug "^4.1.0"
+ globals "^11.1.0"
+ lodash "^4.17.11"
+
+"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0"
+ integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.17.11"
+ to-fast-properties "^2.0.0"
+
+"@cnakazawa/watch@^1.0.3":
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
+ integrity sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA==
+ dependencies:
+ exec-sh "^0.3.2"
+ minimist "^1.2.0"
+
+"@jest/console@^24.7.1":
+ version "24.7.1"
+ resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545"
+ integrity sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg==
+ dependencies:
+ "@jest/source-map" "^24.3.0"
+ chalk "^2.0.1"
+ slash "^2.0.0"
+
+"@jest/core@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.8.0.tgz#fbbdcd42a41d0d39cddbc9f520c8bab0c33eed5b"
+ integrity sha512-R9rhAJwCBQzaRnrRgAdVfnglUuATXdwTRsYqs6NMdVcAl5euG8LtWDe+fVkN27YfKVBW61IojVsXKaOmSnqd/A==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/reporters" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ graceful-fs "^4.1.15"
+ jest-changed-files "^24.8.0"
+ jest-config "^24.8.0"
+ jest-haste-map "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve-dependencies "^24.8.0"
+ jest-runner "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-snapshot "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ jest-watcher "^24.8.0"
+ micromatch "^3.1.10"
+ p-each-series "^1.0.0"
+ pirates "^4.0.1"
+ realpath-native "^1.1.0"
+ rimraf "^2.5.4"
+ strip-ansi "^5.0.0"
+
+"@jest/environment@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.8.0.tgz#0342261383c776bdd652168f68065ef144af0eac"
+ integrity sha512-vlGt2HLg7qM+vtBrSkjDxk9K0YtRBi7HfRFaDxoRtyi+DyVChzhF20duvpdAnKVBV6W5tym8jm0U9EfXbDk1tw==
+ dependencies:
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ jest-mock "^24.8.0"
+
+"@jest/fake-timers@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.8.0.tgz#2e5b80a4f78f284bcb4bd5714b8e10dd36a8d3d1"
+ integrity sha512-2M4d5MufVXwi6VzZhJ9f5S/wU4ud2ck0kxPof1Iz3zWx6Y+V2eJrES9jEktB6O3o/oEyk+il/uNu9PvASjWXQw==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-mock "^24.8.0"
+
+"@jest/reporters@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.8.0.tgz#075169cd029bddec54b8f2c0fc489fd0b9e05729"
+ integrity sha512-eZ9TyUYpyIIXfYCrw0UHUWUvE35vx5I92HGMgS93Pv7du+GHIzl+/vh8Qj9MCWFK/4TqyttVBPakWMOfZRIfxw==
+ dependencies:
+ "@jest/environment" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.2"
+ istanbul-lib-coverage "^2.0.2"
+ istanbul-lib-instrument "^3.0.1"
+ istanbul-lib-report "^2.0.4"
+ istanbul-lib-source-maps "^3.0.1"
+ istanbul-reports "^2.1.1"
+ jest-haste-map "^24.8.0"
+ jest-resolve "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-util "^24.8.0"
+ jest-worker "^24.6.0"
+ node-notifier "^5.2.1"
+ slash "^2.0.0"
+ source-map "^0.6.0"
+ string-length "^2.0.0"
+
+"@jest/source-map@^24.3.0":
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.3.0.tgz#563be3aa4d224caf65ff77edc95cd1ca4da67f28"
+ integrity sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag==
+ dependencies:
+ callsites "^3.0.0"
+ graceful-fs "^4.1.15"
+ source-map "^0.6.0"
+
+"@jest/test-result@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.8.0.tgz#7675d0aaf9d2484caa65e048d9b467d160f8e9d3"
+ integrity sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/types" "^24.8.0"
+ "@types/istanbul-lib-coverage" "^2.0.0"
+
+"@jest/test-sequencer@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.8.0.tgz#2f993bcf6ef5eb4e65e8233a95a3320248cf994b"
+ integrity sha512-OzL/2yHyPdCHXEzhoBuq37CE99nkme15eHkAzXRVqthreWZamEMA0WoetwstsQBCXABhczpK03JNbc4L01vvLg==
+ dependencies:
+ "@jest/test-result" "^24.8.0"
+ jest-haste-map "^24.8.0"
+ jest-runner "^24.8.0"
+ jest-runtime "^24.8.0"
+
+"@jest/transform@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.8.0.tgz#628fb99dce4f9d254c6fd9341e3eea262e06fef5"
+ integrity sha512-xBMfFUP7TortCs0O+Xtez2W7Zu1PLH9bvJgtraN1CDST6LBM/eTOZ9SfwS/lvV8yOfcDpFmwf9bq5cYbXvqsvA==
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/types" "^24.8.0"
+ babel-plugin-istanbul "^5.1.0"
+ chalk "^2.0.1"
+ convert-source-map "^1.4.0"
+ fast-json-stable-stringify "^2.0.0"
+ graceful-fs "^4.1.15"
+ jest-haste-map "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-util "^24.8.0"
+ micromatch "^3.1.10"
+ realpath-native "^1.1.0"
+ slash "^2.0.0"
+ source-map "^0.6.1"
+ write-file-atomic "2.4.1"
+
+"@jest/types@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.8.0.tgz#f31e25948c58f0abd8c845ae26fcea1491dea7ad"
+ integrity sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg==
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ "@types/istanbul-reports" "^1.1.1"
+ "@types/yargs" "^12.0.9"
+
+"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
+ integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
+
+"@protobufjs/base64@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
+ integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
+
+"@protobufjs/codegen@^2.0.4":
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
+ integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
+
+"@protobufjs/eventemitter@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
+ integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
+
+"@protobufjs/fetch@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
+ integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
+ dependencies:
+ "@protobufjs/aspromise" "^1.1.1"
+ "@protobufjs/inquire" "^1.1.0"
+
+"@protobufjs/float@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
+ integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
+
+"@protobufjs/inquire@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
+ integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
+
+"@protobufjs/path@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
+ integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
+
+"@protobufjs/pool@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
+ integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
+
+"@protobufjs/utf8@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
+ integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
+
+"@types/accepts@^1.3.5":
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
+ integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==
+ dependencies:
+ "@types/node" "*"
+
+"@types/babel__core@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.0.tgz#710f2487dda4dcfd010ca6abb2b4dc7394365c51"
+ integrity sha512-wJTeJRt7BToFx3USrCDs2BhEi4ijBInTQjOIukj6a/5tEkwpFMVZ+1ppgmE+Q/FQyc5P/VWUbx7I9NELrKruHA==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc"
+ integrity sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307"
+ integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6":
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.6.tgz#328dd1a8fc4cfe3c8458be9477b219ea158fd7b2"
+ integrity sha512-XYVgHF2sQ0YblLRMLNPB3CkFMewzFmlDsH/TneZFHUXDlABQgh88uOxuez7ZcXxayLFrqLwtDH1t+FmlFwNZxw==
+ dependencies:
+ "@babel/types" "^7.3.0"
+
+"@types/body-parser@*", "@types/body-parser@1.17.0":
+ version "1.17.0"
+ resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c"
+ integrity sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==
+ dependencies:
+ "@types/connect" "*"
+ "@types/node" "*"
+
+"@types/connect@*":
+ version "3.4.32"
+ resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28"
+ integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==
+ dependencies:
+ "@types/node" "*"
+
+"@types/cors@^2.8.4":
+ version "2.8.4"
+ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.4.tgz#50991a759a29c0b89492751008c6af7a7c8267b0"
+ integrity sha512-ipZjBVsm2tF/n8qFGOuGBkUij9X9ZswVi9G3bx/6dz7POpVa6gVHcj1wsX/LVEn9MMF41fxK/PnZPPoTD1UFPw==
+ dependencies:
+ "@types/express" "*"
+
+"@types/events@*":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
+ integrity sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==
+
+"@types/express-serve-static-core@*":
+ version "4.16.0"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz#fdfe777594ddc1fe8eb8eccce52e261b496e43e7"
+ integrity sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==
+ dependencies:
+ "@types/events" "*"
+ "@types/node" "*"
+ "@types/range-parser" "*"
+
+"@types/express@*", "@types/express@4.17.0", "@types/express@^4.11.1":
+ version "4.17.0"
+ resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.0.tgz#49eaedb209582a86f12ed9b725160f12d04ef287"
+ integrity sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==
+ dependencies:
+ "@types/body-parser" "*"
+ "@types/express-serve-static-core" "*"
+ "@types/serve-static" "*"
+
+"@types/graphql-deduplicator@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@types/graphql-deduplicator/-/graphql-deduplicator-2.0.0.tgz#9e577b8f3feb3d067b0ca756f4a1fb356d533922"
+ integrity sha512-swUwj5hWF1yFzbUXStLJrUa0ksAt11B8+SwhsAjQAX0LYJ1LLioAyuDcJ9bovWbsNzIXJYXLvljSPQw8nR728w==
+
+"@types/graphql@^14.0.0":
+ version "14.0.3"
+ resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.0.3.tgz#389e2e5b83ecdb376d9f98fae2094297bc112c1c"
+ integrity sha512-TcFkpEjcQK7w8OcrQcd7iIBPjU0rdyi3ldj6d0iJ4PPSzbWqPBvXj9KSwO14hTOX2dm9RoiH7VuxksJLNYdXUQ==
+
+"@types/istanbul-lib-coverage@*":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
+ integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==
+
+"@types/istanbul-lib-coverage@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.0.tgz#1eb8c033e98cf4e1a4cedcaf8bcafe8cb7591e85"
+ integrity sha512-eAtOAFZefEnfJiRFQBGw1eYqa5GTLCZ1y86N0XSI/D6EB+E8z6VPV/UL7Gi5UEclFqoQk+6NRqEDsfmDLXn8sg==
+
+"@types/istanbul-lib-report@*":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c"
+ integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+
+"@types/istanbul-reports@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a"
+ integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+ "@types/istanbul-lib-report" "*"
+
+"@types/long@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef"
+ integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==
+
+"@types/mime@*":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b"
+ integrity sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==
+
+"@types/node@*", "@types/node@^10.1.0":
+ version "10.12.18"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
+ integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
+
+"@types/range-parser@*":
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
+ integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
+
+"@types/serve-static@*":
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48"
+ integrity sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==
+ dependencies:
+ "@types/express-serve-static-core" "*"
+ "@types/mime" "*"
+
+"@types/stack-utils@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
+ integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
+
+"@types/ws@^6.0.0":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.1.tgz#ca7a3f3756aa12f62a0a62145ed14c6db25d5a28"
+ integrity sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==
+ dependencies:
+ "@types/events" "*"
+ "@types/node" "*"
+
+"@types/yargs@^12.0.2", "@types/yargs@^12.0.9":
+ version "12.0.9"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0"
+ integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==
+
+"@types/yup@0.26.20":
+ version "0.26.20"
+ resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.20.tgz#3b85a05f5dd76e2e8475abb6a8aeae7777627143"
+ integrity sha512-LpCsA6NG7vIU7Umv1k4w3YGIBH5ZLZRPEKo8vJLHVbBUqRy2WaJ002kbsRqcwODpkICAOMuyGOqLQJa5isZ8+g==
+
+"@types/zen-observable@^0.5.3":
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.5.4.tgz#b863a4191e525206819e008097ebf0fb2e3a1cdc"
+ integrity sha512-sW6xN96wUak4tgc89d0tbTg7QDGYhGv5hvQIS6h4mRCd8h2btiZ80loPU8cyLwsBbA4ZeQt0FjvUhJ4rNhdsGg==
+
+"@types/zen-observable@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
+ integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==
+
+"@wry/context@^0.4.0":
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.4.0.tgz#8a8718408e4dd0514a0f8f4231bb4b87130b34e3"
+ integrity sha512-rVjwzFjVYXJ8pWJ8ZRCHv6meOebQvfTlvnUYUNX93Ce0KNeMTqCkf0GiOJc6BNVB96s7qfvwoLN3nUgDnSFOOg==
+ dependencies:
+ tslib "^1.9.3"
+
+"@wry/equality@^0.1.2":
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.7.tgz#512234d078341c32cabda66b89b5dddb5741d9b9"
+ integrity sha512-p1rhJ6PQzpsBr9cMJMHvvx3LQEA28HFX7fAQx6khAX+1lufFeBuk+iRCAyHwj3v6JbpGKvHNa66f+9cpU8c7ew==
+ dependencies:
+ tslib "^1.9.3"
+
+abab@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
+ integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+ integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
+accepts@^1.3.5, accepts@~1.3.7:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
+ integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
+ dependencies:
+ mime-types "~2.1.24"
+ negotiator "0.6.2"
+
+acorn-globals@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.0.tgz#e3b6f8da3c1552a95ae627571f7dd6923bb54103"
+ integrity sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==
+ dependencies:
+ acorn "^6.0.1"
+ acorn-walk "^6.0.1"
+
+acorn-jsx@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
+ integrity sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==
+
+acorn-walk@^6.0.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913"
+ integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==
+
+acorn@^5.5.3:
+ version "5.7.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
+ integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
+
+acorn@^6.0.1:
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754"
+ integrity sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==
+
+acorn@^6.0.7:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818"
+ integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==
+
+activitystrea.ms@~2.1.3:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/activitystrea.ms/-/activitystrea.ms-2.1.3.tgz#553548733e367dc0b6a7badc25fa6f8996cd80c3"
+ integrity sha512-iiG5g5fYgfdaaqqFPaFIZC/KX8/4mOWkvniK+BNwJY6XDDKdIu56wmc9r0x1INHVnbFOTGuM8mZEntaM3I+YXw==
+ dependencies:
+ activitystreams-context "^3.0.0"
+ jsonld "^0.4.11"
+ jsonld-signatures "^1.1.5"
+ moment "^2.17.1"
+ readable-stream "^2.2.3"
+ reasoner "2.0.0"
+ rfc5646 "^2.0.0"
+ vocabs-as "^3.0.0"
+ vocabs-asx "^0.11.1"
+ vocabs-interval "^0.11.1"
+ vocabs-ldp "^0.1.0"
+ vocabs-owl "^0.11.1"
+ vocabs-rdf "^0.11.1"
+ vocabs-rdfs "^0.11.1"
+ vocabs-social "^0.11.1"
+ vocabs-xsd "^0.11.1"
+
+activitystreams-context@>=3.0.0, activitystreams-context@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/activitystreams-context/-/activitystreams-context-3.1.0.tgz#28334e129f17cfb937e8c702c52c1bcb1d2830c7"
+ integrity sha512-KBQ+igwf1tezMXGVw5MvRSEm0gp97JI1hTZ45I6MEkWv25lEgNoA9L6wqfaOiCX8wnMRWw9pwRsPZKypdtxAtg==
+
+ajv@^6.10.0, ajv@^6.5.5, ajv@^6.9.1:
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
+ integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
+ dependencies:
+ fast-deep-equal "^2.0.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-align@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
+ integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=
+ dependencies:
+ string-width "^2.0.0"
+
+ansi-escapes@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30"
+ integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==
+
+ansi-escapes@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
+ integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+ integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+ integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-regex@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9"
+ integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+any-promise@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
+ integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+apollo-cache-control@0.7.4:
+ version "0.7.4"
+ resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.4.tgz#0cb5c7be0e0dd0c44b1257144cd7f9f2a3c374e6"
+ integrity sha512-TVACHwcEF4wfHo5H9FLnoNjo0SLDo2jPW+bXs9aw0Y4Z2UisskSAPnIYOqUPnU8SoeNvs7zWgbLizq11SRTJtg==
+ dependencies:
+ apollo-server-env "2.4.0"
+ graphql-extensions "0.7.4"
+
+apollo-cache-control@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz#173d14ceb3eb9e7cb53de7eb8b61bee6159d4171"
+ integrity sha512-XJQs167e9u+e5ybSi51nGYr70NPBbswdvTEHtbtXbwkZ+n9t0SLPvUcoqceayOSwjK1XYOdU/EKPawNdb3rLQA==
+ dependencies:
+ graphql-extensions "^0.0.x"
+
+apollo-cache-inmemory@~1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.2.tgz#bbf2e4e1eacdf82b2d526f5c2f3b37e5acee3c5e"
+ integrity sha512-AyCl3PGFv5Qv1w4N9vlg63GBPHXgMCekZy5mhlS042ji0GW84uTySX+r3F61ZX3+KM1vA4m9hQyctrEGiv5XjQ==
+ dependencies:
+ apollo-cache "^1.3.2"
+ apollo-utilities "^1.3.2"
+ optimism "^0.9.0"
+ ts-invariant "^0.4.0"
+ tslib "^1.9.3"
+
+apollo-cache@1.3.2, apollo-cache@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.2.tgz#df4dce56240d6c95c613510d7e409f7214e6d26a"
+ integrity sha512-+KA685AV5ETEJfjZuviRTEImGA11uNBp/MJGnaCvkgr+BYRrGLruVKBv6WvyFod27WEB2sp7SsG8cNBKANhGLg==
+ dependencies:
+ apollo-utilities "^1.3.2"
+ tslib "^1.9.3"
+
+apollo-client@~2.6.3:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.3.tgz#9bb2d42fb59f1572e51417f341c5f743798d22db"
+ integrity sha512-DS8pmF5CGiiJ658dG+mDn8pmCMMQIljKJSTeMNHnFuDLV0uAPZoeaAwVFiAmB408Ujqt92oIZ/8yJJAwSIhd4A==
+ dependencies:
+ "@types/zen-observable" "^0.8.0"
+ apollo-cache "1.3.2"
+ apollo-link "^1.0.0"
+ apollo-utilities "1.3.2"
+ symbol-observable "^1.0.2"
+ ts-invariant "^0.4.0"
+ tslib "^1.9.3"
+ zen-observable "^0.8.0"
+
+apollo-datasource@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.5.0.tgz#7a8c97e23da7b9c15cb65103d63178ab19eca5e9"
+ integrity sha512-SVXxJyKlWguuDjxkY/WGlC/ykdsTmPxSF0z8FenagcQ91aPURXzXP1ZDz5PbamY+0iiCRubazkxtTQw4GWTFPg==
+ dependencies:
+ apollo-server-caching "0.4.0"
+ apollo-server-env "2.4.0"
+
+apollo-engine-reporting-protobuf@0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.3.1.tgz#a581257fa8e3bb115ce38bf1b22e052d1475ad69"
+ integrity sha512-Ui3nPG6BSZF8BEqxFs6EkX6mj2OnFLMejxEHSOdM82bakyeouCGd7J0fiy8AD6liJoIyc4X7XfH4ZGGMvMh11A==
+ dependencies:
+ protobufjs "^6.8.6"
+
+apollo-engine-reporting@1.3.5:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.5.tgz#075424d39dfe77a20f96e8e33b7ae52d58c38e1e"
+ integrity sha512-pSwjPgXK/elFsR22LXALtT3jI4fpEpeTNTHgNwLVLohaolusMYgBc/9FnVyFWFfMFS9k+3RmfeQdHhZ6T7WKFQ==
+ dependencies:
+ apollo-engine-reporting-protobuf "0.3.1"
+ apollo-graphql "^0.3.3"
+ apollo-server-core "2.6.7"
+ apollo-server-env "2.4.0"
+ async-retry "^1.2.1"
+ graphql-extensions "0.7.6"
+
+apollo-env@0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.5.1.tgz#b9b0195c16feadf0fe9fd5563edb0b9b7d9e97d3"
+ integrity sha512-fndST2xojgSdH02k5hxk1cbqA9Ti8RX4YzzBoAB4oIe1Puhq7+YlhXGXfXB5Y4XN0al8dLg+5nAkyjNAR2qZTw==
+ dependencies:
+ core-js "^3.0.1"
+ node-fetch "^2.2.0"
+ sha.js "^2.4.11"
+
+apollo-errors@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/apollo-errors/-/apollo-errors-1.9.0.tgz#f1ed0ca0a6be5cd2f24e2eaa7b0860a10146ff51"
+ integrity sha512-XVukHd0KLvgY6tNjsPS3/Re3U6RQlTKrTbIpqqeTMo2N34uQMr+H1UheV21o8hOZBAFosvBORVricJiP5vfmrw==
+ dependencies:
+ assert "^1.4.1"
+ extendable-error "^0.1.5"
+
+apollo-graphql@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.3.tgz#ce1df194f6e547ad3ce1e35b42f9c211766e1658"
+ integrity sha512-t3CO/xIDVsCG2qOvx2MEbuu4b/6LzQjcBBwiVnxclmmFyAxYCIe7rpPlnLHSq7HyOMlCWDMozjoeWfdqYSaLqQ==
+ dependencies:
+ apollo-env "0.5.1"
+ lodash.sortby "^4.7.0"
+
+apollo-link-context@~1.0.18:
+ version "1.0.18"
+ resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.18.tgz#9e700e3314da8ded50057fee0a18af2bfcedbfc3"
+ integrity sha512-aG5cbUp1zqOHHQjAJXG7n/izeMQ6LApd/whEF5z6qZp5ATvcyfSNkCfy3KRJMMZZ3iNfVTs6jF+IUA8Zvf+zeg==
+ dependencies:
+ apollo-link "^1.2.12"
+ tslib "^1.9.3"
+
+apollo-link-http-common@^0.2.14:
+ version "0.2.14"
+ resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.14.tgz#d3a195c12e00f4e311c417f121181dcc31f7d0c8"
+ integrity sha512-v6mRU1oN6XuX8beVIRB6OpF4q1ULhSnmy7ScnHnuo1qV6GaFmDcbdvXqxIkAV1Q8SQCo2lsv4HeqJOWhFfApOg==
+ dependencies:
+ apollo-link "^1.2.12"
+ ts-invariant "^0.4.0"
+ tslib "^1.9.3"
+
+apollo-link-http@~1.5.15:
+ version "1.5.15"
+ resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.15.tgz#106ab23bb8997bd55965d05855736d33119652cf"
+ integrity sha512-epZFhCKDjD7+oNTVK3P39pqWGn4LEhShAoA1Q9e2tDrBjItNfviiE33RmcLcCURDYyW5JA6SMgdODNI4Is8tvQ==
+ dependencies:
+ apollo-link "^1.2.12"
+ apollo-link-http-common "^0.2.14"
+ tslib "^1.9.3"
+
+apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.3:
+ version "1.2.12"
+ resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429"
+ integrity sha512-fsgIAXPKThyMVEMWQsUN22AoQI+J/pVXcjRGAShtk97h7D8O+SPskFinCGEkxPeQpE83uKaqafB2IyWdjN+J3Q==
+ dependencies:
+ apollo-utilities "^1.3.0"
+ ts-invariant "^0.4.0"
+ tslib "^1.9.3"
+ zen-observable-ts "^0.8.19"
+
+apollo-server-caching@0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.4.0.tgz#e82917590d723c0adc1fa52900e79e93ad65e4d9"
+ integrity sha512-GTOZdbLhrSOKYNWMYgaqX5cVNSMT0bGUTZKV8/tYlyYmsB6ey7l6iId3Q7UpHS6F6OR2lstz5XaKZ+T3fDfPzQ==
+ dependencies:
+ lru-cache "^5.0.0"
+
+apollo-server-core@2.6.7:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.7.tgz#85b0310f40cfec43a702569c73af16d88776a6f0"
+ integrity sha512-HfOGLvEwPgDWTvd3ZKRPEkEnICKb7xadn1Mci4+auMTsL/NVkfpjPa8cdzubi/kS2/MvioIn7Bg74gmiSLghGQ==
+ dependencies:
+ "@apollographql/apollo-tools" "^0.3.6"
+ "@apollographql/graphql-playground-html" "1.6.20"
+ "@types/ws" "^6.0.0"
+ apollo-cache-control "0.7.4"
+ apollo-datasource "0.5.0"
+ apollo-engine-reporting "1.3.5"
+ apollo-server-caching "0.4.0"
+ apollo-server-env "2.4.0"
+ apollo-server-errors "2.3.0"
+ apollo-server-plugin-base "0.5.6"
+ apollo-tracing "0.7.3"
+ fast-json-stable-stringify "^2.0.0"
+ graphql-extensions "0.7.6"
+ graphql-subscriptions "^1.0.0"
+ graphql-tag "^2.9.2"
+ graphql-tools "^4.0.0"
+ graphql-upload "^8.0.2"
+ sha.js "^2.4.11"
+ subscriptions-transport-ws "^0.9.11"
+ ws "^6.0.0"
+
+apollo-server-core@^1.3.6, apollo-server-core@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.4.0.tgz#4faff7f110bfdd6c3f47008302ae24140f94c592"
+ integrity sha512-BP1Vh39krgEjkQxbjTdBURUjLHbFq1zeOChDJgaRsMxGtlhzuLWwwC6lLdPatN8jEPbeHq8Tndp9QZ3iQZOKKA==
+ dependencies:
+ apollo-cache-control "^0.1.0"
+ apollo-tracing "^0.1.0"
+ graphql-extensions "^0.0.x"
+
+apollo-server-env@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.0.tgz#6611556c6b627a1636eed31317d4f7ea30705872"
+ integrity sha512-7ispR68lv92viFeu5zsRUVGP+oxsVI3WeeBNniM22Cx619maBUwcYTIC3+Y3LpXILhLZCzA1FASZwusgSlyN9w==
+ dependencies:
+ node-fetch "^2.1.2"
+ util.promisify "^1.0.0"
+
+apollo-server-errors@2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061"
+ integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw==
+
+apollo-server-express@2.6.7:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.7.tgz#22307e08b75be1553f4099d00028abe52597767d"
+ integrity sha512-qbCQM+8LxXpwPNN5Sdvcb+Sne8zuCORFt25HJtPJRkHlyBUzOd7JA7SEnUn5e2geTiiGoVIU5leh+++C51udTw==
+ dependencies:
+ "@apollographql/graphql-playground-html" "1.6.20"
+ "@types/accepts" "^1.3.5"
+ "@types/body-parser" "1.17.0"
+ "@types/cors" "^2.8.4"
+ "@types/express" "4.17.0"
+ accepts "^1.3.5"
+ apollo-server-core "2.6.7"
+ body-parser "^1.18.3"
+ cors "^2.8.4"
+ graphql-subscriptions "^1.0.0"
+ graphql-tools "^4.0.0"
+ type-is "^1.6.16"
+
+apollo-server-express@^1.3.6:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-1.4.0.tgz#7d7c58d6d6f9892b83fe575669093bb66738b125"
+ integrity sha512-zkH00nxhLnJfO0HgnNPBTfZw8qI5ILaPZ5TecMCI9+Y9Ssr2b0bFr9pBRsXy9eudPhI+/O4yqegSUsnLdF/CPw==
+ dependencies:
+ apollo-server-core "^1.4.0"
+ apollo-server-module-graphiql "^1.4.0"
+
+apollo-server-lambda@1.3.6:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/apollo-server-lambda/-/apollo-server-lambda-1.3.6.tgz#bdaac37f143c6798e40b8ae75580ba673cea260e"
+ integrity sha1-varDfxQ8Z5jkC4rnVYC6ZzzqJg4=
+ dependencies:
+ apollo-server-core "^1.3.6"
+ apollo-server-module-graphiql "^1.3.4"
+
+apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec"
+ integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA==
+
+apollo-server-plugin-base@0.5.6:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.6.tgz#3a7128437a0f845e7d873fa43ef091ff7bf27975"
+ integrity sha512-wJvcPqfm/kiBwY5JZT85t2A4pcHv24xdQIpWMNt1zsnx77lIZqJmhsc22eSUSrlnYqUMXC4XMVgSUfAO4oI9wg==
+
+apollo-server-testing@~2.6.7:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.7.tgz#cfc6366921eb99fd0cbc5d02552a8a5b268787d5"
+ integrity sha512-lqgZuSqBd5hkRILeVEleo2ScJjukR/E71Mv67vPBUs01s0gEHNnjSRnuOJJOM3cAFBQOdKPc42cHGANzf2ZZTw==
+ dependencies:
+ apollo-server-core "2.6.7"
+
+apollo-server@~2.6.7:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.7.tgz#b707ede529b4d45f2f00a74f3b457658b0e62e83"
+ integrity sha512-4wk9JykURLed6CnNIj9jhU6ueeTVmGBTyAnnvnlhRrOf50JAFszUErZIKg6lw5vVr5riaByrGFIkMBTySCHgPQ==
+ dependencies:
+ apollo-server-core "2.6.7"
+ apollo-server-express "2.6.7"
+ express "^4.0.0"
+ graphql-subscriptions "^1.0.0"
+ graphql-tools "^4.0.0"
+
+apollo-tracing@0.7.3:
+ version "0.7.3"
+ resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.3.tgz#8533e3e2dca2d5a25e8439ce498ea33ff4d159ee"
+ integrity sha512-H6fSC+awQGnfDyYdGIB0UQUhcUC3n5Vy+ujacJ0bY6R+vwWeZOQvu7wRHNjk/rbOSTLCo9A0OcVX7huRyu9SZg==
+ dependencies:
+ apollo-server-env "2.4.0"
+ graphql-extensions "0.7.4"
+
+apollo-tracing@^0.1.0:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.1.4.tgz#5b8ae1b01526b160ee6e552a7f131923a9aedcc7"
+ integrity sha512-Uv+1nh5AsNmC3m130i2u3IqbS+nrxyVV3KYimH5QKsdPjxxIQB3JAT+jJmpeDxBel8gDVstNmCh82QSLxLSIdQ==
+ dependencies:
+ graphql-extensions "~0.0.9"
+
+apollo-upload-server@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/apollo-upload-server/-/apollo-upload-server-7.1.0.tgz#21e07b52252b3749b913468599813e13cfca805f"
+ integrity sha512-cD9ReCeyurYwZyEDqJYb5TOc9dt8yhPzS+MtrY3iJdqw+pqiiyPngAvVXHjN+Ca7Lajvom4/AT/PBrYVDMM3Kw==
+ dependencies:
+ busboy "^0.2.14"
+ fs-capacitor "^1.0.0"
+ http-errors "^1.7.0"
+ object-path "^0.11.4"
+
+apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
+ integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==
+ dependencies:
+ "@wry/equality" "^0.1.2"
+ fast-json-stable-stringify "^2.0.0"
+ ts-invariant "^0.4.0"
+ tslib "^1.9.3"
+
+aproba@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+ integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
+are-we-there-yet@~1.1.2:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
+ integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+argparse@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+ integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+ integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+ integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+ integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
+
+array-filter@~0.0.0:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
+ integrity sha1-fajPLiZijtcygDWB/SH2fKzS7uw=
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+ integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
+
+array-includes@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
+ integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.7.0"
+
+array-map@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
+ integrity sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=
+
+array-reduce@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
+ integrity sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=
+
+array-uniq@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+ integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+ integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+arrify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+ integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
+
+asn1@~0.2.3:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+ integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+ dependencies:
+ safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+ integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assert@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+ integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=
+ dependencies:
+ util "0.10.3"
+
+assertion-error-formatter@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/assertion-error-formatter/-/assertion-error-formatter-2.0.1.tgz#6bbdffaec8e2fa9e2b0eb158bfe353132d7c0a9b"
+ integrity sha512-cjC3jUCh9spkroKue5PDSKH5RFQ/KNuZJhk3GwHYmB/8qqETxLOmMdLH+ohi/VukNzxDlMvIe7zScvLoOdhb6Q==
+ dependencies:
+ diff "^3.0.0"
+ pad-right "^0.2.2"
+ repeat-string "^1.6.1"
+
+assertion-error@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
+ integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
+assignment@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/assignment/-/assignment-2.0.0.tgz#ffd17b21bf5d6b22e777b989681a815456a3dd3e"
+ integrity sha1-/9F7Ib9dayLnd7mJaBqBVFaj3T4=
+
+assignment@2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/assignment/-/assignment-2.2.0.tgz#f5b5bc2d160d69986e8700cd38f567c0aabe101e"
+ integrity sha1-9bW8LRYNaZhuhwDNOPVnwKq+EB4=
+
+astral-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+ integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+
+async-each@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+ integrity sha1-GdOGodntxufByF04iu28xW0zYC0=
+
+async-limiter@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
+ integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
+
+async-retry@^1.2.1:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.2.3.tgz#a6521f338358d322b1a0012b79030c6f411d1ce0"
+ integrity sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q==
+ dependencies:
+ retry "0.12.0"
+
+async@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+ integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
+
+async@^2.5.0:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
+ integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==
+ dependencies:
+ lodash "^4.17.10"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+atob@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+ integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
+aws-lambda@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/aws-lambda/-/aws-lambda-0.1.2.tgz#19b1585075df31679597b976a5f1def61f12ccee"
+ integrity sha1-GbFYUHXfMWeVl7l2pfHe9h8SzO4=
+ dependencies:
+ aws-sdk "^*"
+ commander "^2.5.0"
+ dotenv "^0.4.0"
+
+aws-sdk@^*:
+ version "2.373.0"
+ resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.373.0.tgz#fcc5606634b3b11d80810ad252d1b52b3733d780"
+ integrity sha512-NZYXwXGtFt9jxaKXc+PJsLPnpbD03t0MAZRxh93g36kbFMuRXtY8CDqHYNQ0ZcrgQpXbCQiz1fxT5/wu5Cu70g==
+ dependencies:
+ buffer "4.9.1"
+ events "1.1.1"
+ ieee754 "1.1.8"
+ jmespath "0.15.0"
+ querystring "0.2.0"
+ sax "1.2.1"
+ url "0.10.3"
+ uuid "3.1.0"
+ 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"
+ integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+ integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
+
+babel-core@~7.0.0-0:
+ version "7.0.0-bridge.0"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece"
+ integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==
+
+babel-eslint@~10.0.2:
+ version "10.0.2"
+ resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.2.tgz#182d5ac204579ff0881684b040560fdcc1558456"
+ integrity sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/parser" "^7.0.0"
+ "@babel/traverse" "^7.0.0"
+ "@babel/types" "^7.0.0"
+ eslint-scope "3.7.1"
+ eslint-visitor-keys "^1.0.0"
+
+babel-jest@^24.8.0, babel-jest@~24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.8.0.tgz#5c15ff2b28e20b0f45df43fe6b7f2aae93dba589"
+ integrity sha512-+5/kaZt4I9efoXzPlZASyK/lN9qdRKmmUav9smVc0ruPQD7IsfucQ87gpOE8mn2jbDuS6M/YOW6n3v9ZoIfgnw==
+ dependencies:
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/babel__core" "^7.1.0"
+ babel-plugin-istanbul "^5.1.0"
+ babel-preset-jest "^24.6.0"
+ chalk "^2.4.2"
+ slash "^2.0.0"
+
+babel-plugin-istanbul@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.0.tgz#6892f529eff65a3e2d33d87dc5888ffa2ecd4a30"
+ integrity sha512-CLoXPRSUWiR8yao8bShqZUIC6qLfZVVY3X1wj+QPNXu0wfmrRRfarh1LYy+dYMVI+bDj0ghy3tuqFFRFZmL1Nw==
+ dependencies:
+ find-up "^3.0.0"
+ istanbul-lib-instrument "^3.0.0"
+ test-exclude "^5.0.0"
+
+babel-plugin-jest-hoist@^24.6.0:
+ version "24.6.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.6.0.tgz#f7f7f7ad150ee96d7a5e8e2c5da8319579e78019"
+ integrity sha512-3pKNH6hMt9SbOv0F3WVmy5CWQ4uogS3k0GY5XLyQHJ9EGpAT9XWkFd2ZiXXtkwFHdAHa5j7w7kfxSP5lAIwu7w==
+ dependencies:
+ "@types/babel__traverse" "^7.0.6"
+
+babel-preset-jest@^24.6.0:
+ version "24.6.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz#66f06136eefce87797539c0d63f1769cc3915984"
+ integrity sha512-pdZqLEdmy1ZK5kyRUfvBb2IfTPb2BUvIJczlPspS8fWmBQslNNDBqVfh7BW5leOVJMDZKzjD8XEyABTk6gQ5yw==
+ dependencies:
+ "@babel/plugin-syntax-object-rest-spread" "^7.0.0"
+ babel-plugin-jest-hoist "^24.6.0"
+
+babel-runtime@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+ integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.11.0"
+
+backo2@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
+ integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
+
+balanced-match@^1.0.0:
+ version "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.0"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
+ integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==
+
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+ integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+ dependencies:
+ tweetnacl "^0.14.3"
+
+bcryptjs@~2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
+ integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=
+
+becke-ch--regex--s0-0-v1--base--pl--lib@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/becke-ch--regex--s0-0-v1--base--pl--lib/-/becke-ch--regex--s0-0-v1--base--pl--lib-1.4.0.tgz#429ceebbfa5f7e936e78d73fbdc7da7162b20e20"
+ integrity sha1-Qpzuu/pffpNueNc/vcfacWKyDiA=
+
+binary-extensions@^1.0.0:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14"
+ integrity sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==
+
+bitcore-lib@^0.13.7:
+ version "0.13.19"
+ resolved "https://registry.yarnpkg.com/bitcore-lib/-/bitcore-lib-0.13.19.tgz#48af1e9bda10067c1ab16263472b5add2000f3dc"
+ integrity sha1-SK8em9oQBnwasWJjRyta3SAA89w=
+ dependencies:
+ bn.js "=2.0.4"
+ bs58 "=2.0.0"
+ buffer-compare "=1.0.0"
+ elliptic "=3.0.3"
+ inherits "=2.0.1"
+ lodash "=3.10.1"
+
+"bitcore-message@github:CoMakery/bitcore-message#dist":
+ version "1.0.2"
+ resolved "https://codeload.github.com/CoMakery/bitcore-message/tar.gz/8799cc327029c3d34fc725f05b2cf981363f6ebf"
+ dependencies:
+ bitcore-lib "^0.13.7"
+
+bluebird@^3.4.1:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
+ integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==
+
+bn.js@=2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.0.4.tgz#220a7cd677f7f1bfa93627ff4193776fe7819480"
+ integrity sha1-Igp81nf38b+pNif/QZN3b+eBlIA=
+
+bn.js@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.2.0.tgz#12162bc2ae71fc40a5626c33438f3a875cd37625"
+ integrity sha1-EhYrwq5x/EClYmwzQ486h1zTdiU=
+
+body-parser-graphql@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/body-parser-graphql/-/body-parser-graphql-1.1.0.tgz#80a80353c7cb623562fd375750dfe018d75f0f7c"
+ integrity sha512-bOBF4n1AnUjcY1SzLeibeIx4XOuYqEkjn/Lm4yKhnN6KedoXMv4hVqgcKHGRnxOMJP64tErqrQU+4cihhpbJXg==
+ dependencies:
+ body-parser "^1.18.2"
+
+body-parser@1.19.0, body-parser@^1.18.2, body-parser@^1.18.3:
+ version "1.19.0"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
+ integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
+ dependencies:
+ bytes "3.1.0"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.2"
+ http-errors "1.7.2"
+ iconv-lite "0.4.24"
+ on-finished "~2.3.0"
+ qs "6.7.0"
+ raw-body "2.4.0"
+ type-is "~1.6.17"
+
+boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+ integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+boxen@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
+ integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==
+ dependencies:
+ ansi-align "^2.0.0"
+ camelcase "^4.0.0"
+ chalk "^2.0.1"
+ cli-boxes "^1.0.0"
+ string-width "^2.0.0"
+ term-size "^1.2.0"
+ widest-line "^2.0.0"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^2.3.1, braces@^2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+brorand@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+ integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+
+browser-process-hrtime@^0.1.2:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4"
+ integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==
+
+browser-resolve@^1.11.3:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
+ integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==
+ dependencies:
+ resolve "1.1.7"
+
+browserslist@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.0.tgz#5274028c26f4d933d5b1323307c1d1da5084c9ff"
+ integrity sha512-Jk0YFwXBuMOOol8n6FhgkDzn3mY9PYLYGk29zybF05SbRTsMgPqmTNeQQhOghCxq5oFqAXE3u4sYddr4C0uRhg==
+ dependencies:
+ caniuse-lite "^1.0.30000967"
+ electron-to-chromium "^1.3.133"
+ node-releases "^1.1.19"
+
+bs58@=2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bs58/-/bs58-2.0.0.tgz#72b713bed223a0ac518bbda0e3ce3f4817f39eb5"
+ integrity sha1-crcTvtIjoKxRi72g484/SBfznrU=
+
+bser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
+ integrity sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=
+ dependencies:
+ node-int64 "^0.4.0"
+
+buffer-compare@=1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-compare/-/buffer-compare-1.0.0.tgz#acaa7a966e98eee9fae14b31c39a5f158fb3c4a2"
+ integrity sha1-rKp6lm6Y7un64Usxw5pfFY+zxKI=
+
+buffer-equal-constant-time@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+ integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
+
+buffer-from@^1.0.0:
+ version "1.1.1"
+ 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"
+
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+ integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
+
+busboy@^0.2.14:
+ version "0.2.14"
+ resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
+ integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=
+ dependencies:
+ dicer "0.2.5"
+ readable-stream "1.1.x"
+
+busboy@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b"
+ integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==
+ dependencies:
+ dicer "0.3.0"
+
+bytes@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
+ integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
+callsites@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.0.0.tgz#fb7eb569b72ad7a45812f93fd9430a3e410b3dd3"
+ integrity sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw==
+
+camelcase@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+ integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
+
+camelcase@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
+ integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
+
+camelize@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
+ integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=
+
+caniuse-lite@^1.0.30000967:
+ version "1.0.30000971"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000971.tgz#d1000e4546486a6977756547352bc96a4cfd2b13"
+ integrity sha512-TQFYFhRS0O5rdsmSbF1Wn+16latXYsQJat66f7S7lizXW1PVpWJeZw9wqqVLIjuxDRz7s7xRUj13QCfd8hKn6g==
+
+capture-exit@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f"
+ integrity sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=
+ dependencies:
+ rsvp "^3.3.3"
+
+capture-stack-trace@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d"
+ integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+chai@~4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5"
+ integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==
+ dependencies:
+ assertion-error "^1.1.0"
+ check-error "^1.0.2"
+ deep-eql "^3.0.1"
+ get-func-name "^2.0.0"
+ pathval "^1.1.0"
+ type-detect "^4.0.5"
+
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chardet@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
+ integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+
+check-error@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
+ integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=
+
+cheerio@~1.0.0-rc.3:
+ version "1.0.0-rc.3"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
+ integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
+ dependencies:
+ css-select "~1.2.0"
+ dom-serializer "~0.1.1"
+ entities "~1.1.1"
+ htmlparser2 "^3.9.1"
+ lodash "^4.15.0"
+ parse5 "^3.0.1"
+
+chokidar@^2.0.4, chokidar@^2.1.5:
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.5.tgz#0ae8434d962281a5f56c72869e79cb6d9d86ad4d"
+ integrity sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==
+ dependencies:
+ anymatch "^2.0.0"
+ async-each "^1.0.1"
+ braces "^2.3.2"
+ glob-parent "^3.1.0"
+ inherits "^2.0.3"
+ is-binary-path "^1.0.0"
+ is-glob "^4.0.0"
+ normalize-path "^3.0.0"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.2.1"
+ upath "^1.1.1"
+ optionalDependencies:
+ fsevents "^1.2.7"
+
+chownr@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
+ integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
+
+ci-info@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
+ integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==
+
+ci-info@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
+ integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
+
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+cli-boxes@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
+ integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM=
+
+cli-cursor@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+ integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
+ dependencies:
+ restore-cursor "^2.0.0"
+
+cli-table3@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202"
+ integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==
+ dependencies:
+ object-assign "^4.1.0"
+ string-width "^2.1.1"
+ optionalDependencies:
+ colors "^1.1.2"
+
+cli-width@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
+ integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
+
+cliui@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
+ integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==
+ dependencies:
+ string-width "^2.1.1"
+ strip-ansi "^4.0.0"
+ wrap-ansi "^2.0.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+ integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+ integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+colors@^1.1.2:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d"
+ integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
+ integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@^2.5.0, commander@^2.8.1, commander@^2.9.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
+ integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
+
+commander@~2.17.1:
+ version "2.17.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
+ integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
+
+commander@~2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
+ integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=
+ dependencies:
+ graceful-readlink ">= 1.0.0"
+
+commondir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+ integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
+
+component-emitter@^1.2.0, component-emitter@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+ integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+configstore@^3.0.0:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f"
+ integrity sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==
+ dependencies:
+ dot-prop "^4.1.0"
+ graceful-fs "^4.1.2"
+ make-dir "^1.0.0"
+ unique-string "^1.0.0"
+ write-file-atomic "^2.0.0"
+ xdg-basedir "^3.0.0"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+ integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
+
+contains-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+ integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
+
+content-disposition@0.5.3:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
+ integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
+ dependencies:
+ safe-buffer "5.1.2"
+
+content-security-policy-builder@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz#8749a1d542fcbe82237281ea9f716ce68b394dd2"
+ integrity sha512-j+Nhmj1yfZAikJLImCvPJFE29x/UuBi+/MWqggGGc515JKaZrjuei2RhULJmy0MsstW3E3htl002bwmBNMKr7w==
+
+content-type@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+ integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+
+convert-source-map@^1.1.0, convert-source-map@^1.4.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
+ integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==
+ dependencies:
+ safe-buffer "~5.1.1"
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+ integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
+
+cookie@0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
+ integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
+
+cookiejar@^2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
+ integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==
+
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+ integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
+core-js-compat@^3.1.1:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.1.2.tgz#c29ab9722517094b98622175e2218c3b7398176d"
+ integrity sha512-X0Ch5f6itrHxhg5HSJucX6nNLNAGr+jq+biBh6nPGc3YAWz2a8p/ZIZY8cUkDzSRNG54omAuu3hoEF8qZbu/6Q==
+ dependencies:
+ browserslist "^4.6.0"
+ core-js-pure "3.1.2"
+ semver "^6.0.0"
+
+core-js-pure@3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.1.2.tgz#62fc435f35b7374b9b782013cdcb2f97e9f6dffa"
+ integrity sha512-5ckIdBF26B3ldK9PM177y2ZcATP2oweam9RskHSoqfZCrJ2As6wVg8zJ1zTriFsZf6clj/N1ThDFRGaomMsh9w==
+
+core-js@^2.4.0, core-js@^2.5.3, core-js@^2.5.7:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.2.tgz#267988d7268323b349e20b4588211655f0e83944"
+ integrity sha512-NdBPF/RVwPW6jr0NCILuyN9RiqLo2b1mddWHkUL+VnvcB7dzlnBJ1bXYntjpTGOgkZiiLWj2JxmOr7eGE3qK6g==
+
+core-js@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0.tgz#a8dbfa978d29bfc263bfb66c556d0ca924c28957"
+ integrity sha512-WBmxlgH2122EzEJ6GH8o9L/FeoUKxxxZ6q6VUxoTlsE4EvbTWKJb447eyVxTEuq0LpXjlq/kCB2qgBvsYRkLvQ==
+
+core-js@^3.0.1:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.3.tgz#95700bca5f248f5f78c0ec63e784eca663ec4138"
+ integrity sha512-PWZ+ZfuaKf178BIAg+CRsljwjIMRV8MY00CbZczkR6Zk5LfkSkjGoaab3+bqRQWVITNZxQB7TFYz+CFcyuamvA==
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+ integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+cors@^2.8.4, cors@~2.8.5:
+ version "2.8.5"
+ resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
+ integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
+ dependencies:
+ object-assign "^4"
+ vary "^1"
+
+create-error-class@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
+ integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=
+ dependencies:
+ capture-stack-trace "^1.0.0"
+
+cross-env@~5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2"
+ integrity sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==
+ dependencies:
+ cross-spawn "^6.0.5"
+ is-windows "^1.0.0"
+
+cross-fetch@2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.2.tgz#a47ff4f7fc712daba8f6a695a11c948440d45723"
+ integrity sha1-pH/09/xxLauo9qaVoRyUhEDUVyM=
+ dependencies:
+ node-fetch "2.1.2"
+ whatwg-fetch "2.0.4"
+
+cross-spawn@^5.0.1:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cross-spawn@^6.0.0, cross-spawn@^6.0.5:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+crypto-random-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
+ integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
+
+css-select@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+ integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "~1.0.1"
+
+css-what@2.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.2.tgz#c0876d9d0480927d7d4920dcd72af3595649554d"
+ integrity sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ==
+
+cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
+ version "0.3.4"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797"
+ integrity sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog==
+
+cssstyle@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.1.1.tgz#18b038a9c44d65f7a8e428a653b9f6fe42faf5fb"
+ integrity sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==
+ dependencies:
+ cssom "0.3.x"
+
+cucumber-expressions@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/cucumber-expressions/-/cucumber-expressions-6.0.1.tgz#47c9c573781c2ff721d7ad5b2cd1c97f4399ab8e"
+ integrity sha1-R8nFc3gcL/ch161bLNHJf0OZq44=
+ dependencies:
+ becke-ch--regex--s0-0-v1--base--pl--lib "^1.2.0"
+
+cucumber-tag-expressions@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/cucumber-tag-expressions/-/cucumber-tag-expressions-1.1.1.tgz#7f5c7b70009bc2b666591bfe64854578bedee85a"
+ integrity sha1-f1x7cACbwrZmWRv+ZIVFeL7e6Fo=
+
+cucumber@~5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cucumber/-/cucumber-5.1.0.tgz#7b166812c255bec7eac4b0df7007a40d089c895d"
+ integrity sha512-zrl2VYTBRgvxucwV2GKAvLqcfA1Naeax8plPvWgPEzl3SCJiuPPv3WxBHIRHtPYcEdbHDR6oqLpZP4bJ8UIdmA==
+ dependencies:
+ "@babel/polyfill" "^7.2.3"
+ assertion-error-formatter "^2.0.1"
+ bluebird "^3.4.1"
+ cli-table3 "^0.5.1"
+ colors "^1.1.2"
+ commander "^2.9.0"
+ cross-spawn "^6.0.5"
+ cucumber-expressions "^6.0.0"
+ cucumber-tag-expressions "^1.1.1"
+ duration "^0.2.1"
+ escape-string-regexp "^1.0.5"
+ figures "2.0.0"
+ gherkin "^5.0.0"
+ glob "^7.1.3"
+ indent-string "^3.1.0"
+ is-generator "^1.0.2"
+ is-stream "^1.1.0"
+ knuth-shuffle-seeded "^1.0.6"
+ lodash "^4.17.10"
+ mz "^2.4.0"
+ progress "^2.0.0"
+ resolve "^1.3.3"
+ serialize-error "^3.0.0"
+ stack-chain "^2.0.0"
+ stacktrace-js "^2.0.0"
+ string-argv "0.1.1"
+ title-case "^2.1.1"
+ util-arity "^1.0.2"
+ verror "^1.9.0"
+
+d@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
+ integrity sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=
+ dependencies:
+ es5-ext "^0.10.9"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+ dependencies:
+ assert-plus "^1.0.0"
+
+dasherize@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/dasherize/-/dasherize-2.0.0.tgz#6d809c9cd0cf7bb8952d80fc84fa13d47ddb1308"
+ integrity sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=
+
+data-urls@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
+ integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
+ dependencies:
+ abab "^2.0.0"
+ whatwg-mimetype "^2.2.0"
+ whatwg-url "^7.0.0"
+
+date-fns@2.0.0-beta.2:
+ version "2.0.0-beta.2"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-beta.2.tgz#ccd556df832ef761baa88c600f53d2e829245999"
+ integrity sha512-4cicZF707RNerr3/Q3CcdLo+3OHMCfrRXE7h5iFgn7AMvX07sqKLxSf8Yp+WJW5bvKr2cy9/PkctXLv4iFtOaA==
+
+debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.1.0:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+ integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+ dependencies:
+ ms "^2.1.1"
+
+debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+ integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+ dependencies:
+ ms "^2.1.1"
+
+decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+decode-uri-component@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+ integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
+deep-eql@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
+ integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==
+ dependencies:
+ type-detect "^4.0.0"
+
+deep-extend@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+ integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+deep-is@~0.1.3:
+ version "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@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
+ integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
+
+define-properties@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ dependencies:
+ object-keys "^1.0.12"
+
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+ integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+
+depd@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+ integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+depd@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+ integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+
+deprecated-decorator@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37"
+ integrity sha1-AJZjF7ehL+kvPMgx91g68ym4bDc=
+
+destroy@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+ integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+
+detect-libc@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+ integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
+
+detect-newline@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
+ integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=
+
+dicer@0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f"
+ integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=
+ dependencies:
+ readable-stream "1.1.x"
+ streamsearch "0.1.2"
+
+dicer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872"
+ integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==
+ dependencies:
+ streamsearch "0.1.2"
+
+diff-sequences@^24.3.0:
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975"
+ integrity sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==
+
+diff@^3.0.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
+ integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
+
+dns-prefetch-control@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz#60ddb457774e178f1f9415f0cabb0e85b0b300b2"
+ integrity sha1-YN20V3dOF48flBXwyrsOhbCzALI=
+
+doctrine@1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+ integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.0"
+
+doctrine@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+ integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+ dependencies:
+ esutils "^2.0.2"
+
+dom-serializer@0, dom-serializer@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+ integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+ dependencies:
+ domelementtype "^1.3.0"
+ entities "^1.1.1"
+
+domelementtype@1, domelementtype@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+ integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
+domexception@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+ integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+ dependencies:
+ webidl-conversions "^4.0.2"
+
+domhandler@^2.3.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+ integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+domutils@^1.5.1:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+ integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+dont-sniff-mimetype@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz#5932890dc9f4e2f19e5eb02a20026e5e5efc8f58"
+ integrity sha1-WTKJDcn04vGeXrAqIAJuXl78j1g=
+
+dot-prop@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
+ integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==
+ dependencies:
+ is-obj "^1.0.0"
+
+dotenv@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-0.4.0.tgz#f6fb351363c2d92207245c737802c9ab5ae1495a"
+ integrity sha1-9vs1E2PC2SIHJFxzeALJq1rhSVo=
+
+dotenv@~8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440"
+ integrity sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==
+
+duplexer3@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
+ integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
+
+duration@^0.2.1:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/duration/-/duration-0.2.2.tgz#ddf149bc3bc6901150fe9017111d016b3357f529"
+ integrity sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg==
+ dependencies:
+ d "1"
+ es5-ext "~0.10.46"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+ integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+ dependencies:
+ jsbn "~0.1.0"
+ safer-buffer "^2.1.0"
+
+ecdsa-sig-formatter@1.0.11:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
+ integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
+ dependencies:
+ safe-buffer "^5.0.1"
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
+electron-to-chromium@^1.3.133:
+ version "1.3.137"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.137.tgz#ba7c88024984c038a5c5c434529aabcea7b42944"
+ integrity sha512-kGi32g42a8vS/WnYE7ELJyejRT7hbr3UeOOu0WeuYuQ29gCpg9Lrf6RdcTQVXSt/v0bjCfnlb/EWOOsiKpTmkw==
+
+elliptic@=3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-3.0.3.tgz#865c9b420bfbe55006b9f969f97a0d2c44966595"
+ integrity sha1-hlybQgv75VAGuflp+XoNLESWZZU=
+ dependencies:
+ bn.js "^2.0.0"
+ brorand "^1.0.1"
+ hash.js "^1.0.0"
+ inherits "^2.0.1"
+
+emoji-regex@^7.0.1:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+ integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+
+encodeurl@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+ integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+end-of-stream@^1.1.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
+ integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==
+ dependencies:
+ once "^1.4.0"
+
+entities@^1.1.1, entities@~1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+ integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
+error-ex@^1.2.0, error-ex@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+ integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+ dependencies:
+ is-arrayish "^0.2.1"
+
+error-stack-parser@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.2.tgz#4ae8dbaa2bf90a8b450707b9149dcabca135520d"
+ integrity sha512-E1fPutRDdIj/hohG0UpT5mayXNCxXP9d+snxFsPU9X0XgccOumKraa3juDMwTUyi7+Bu5+mCGagjg4IYeNbOdw==
+ dependencies:
+ stackframe "^1.0.4"
+
+es-abstract@^1.4.3:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
+ integrity sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==
+ dependencies:
+ es-to-primitive "^1.1.1"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+ is-callable "^1.1.3"
+ is-regex "^1.0.4"
+
+es-abstract@^1.5.1, es-abstract@^1.7.0:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
+ integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
+ dependencies:
+ es-to-primitive "^1.2.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ is-callable "^1.1.4"
+ is-regex "^1.0.4"
+ object-keys "^1.0.12"
+
+es-to-primitive@^1.1.1, es-to-primitive@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
+ integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.46:
+ version "0.10.49"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.49.tgz#059a239de862c94494fec28f8150c977028c6c5e"
+ integrity sha512-3NMEhi57E31qdzmYp2jwRArIUsj1HI/RxbQ4bgnSB+AIKIxsAmTiK83bYMifIcpWvEc3P1X30DhUKOqEtF/kvg==
+ dependencies:
+ es6-iterator "~2.0.3"
+ es6-symbol "~3.1.1"
+ next-tick "^1.0.0"
+
+es6-iterator@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+ integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
+ dependencies:
+ d "1"
+ es5-ext "^0.10.35"
+ es6-symbol "^3.1.1"
+
+es6-promise@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-2.3.0.tgz#96edb9f2fdb01995822b263dd8aadab6748181bc"
+ integrity sha1-lu258v2wGZWCKyY92KratnSBgbw=
+
+es6-promise@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-2.0.1.tgz#ccc4963e679f0ca9fb187c777b9e583d3c7573c2"
+ integrity sha1-zMSWPmefDKn7GHx3e55YPTx1c8I=
+
+es6-promise@~4.0.5:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42"
+ integrity sha1-eILzCt3lskDM+n99eMVIMwlRrkI=
+
+es6-symbol@^3.1.1, es6-symbol@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
+ integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+ integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+escodegen@^1.9.1:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589"
+ integrity sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==
+ dependencies:
+ esprima "^3.1.3"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+eslint-config-prettier@~6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.0.0.tgz#f429a53bde9fc7660e6353910fd996d6284d3c25"
+ integrity sha512-vDrcCFE3+2ixNT5H83g28bO/uYAwibJxerXPj+E7op4qzBCsAV36QfvdAyVOoNxKAH2Os/e01T/2x++V0LPukA==
+ dependencies:
+ get-stdin "^6.0.0"
+
+eslint-config-standard@~12.0.0:
+ version "12.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-12.0.0.tgz#638b4c65db0bd5a41319f96bba1f15ddad2107d9"
+ integrity sha512-COUz8FnXhqFitYj4DTqHzidjIL/t4mumGZto5c7DrBpvWoie+Sn3P4sLEzUGeYhRElWuFEf8K1S1EfvD1vixCQ==
+
+eslint-import-resolver-node@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
+ integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==
+ dependencies:
+ debug "^2.6.9"
+ resolve "^1.5.0"
+
+eslint-module-utils@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz#8b93499e9b00eab80ccb6614e69f03678e84e09a"
+ integrity sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw==
+ dependencies:
+ debug "^2.6.8"
+ pkg-dir "^2.0.0"
+
+eslint-plugin-es@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6"
+ integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw==
+ dependencies:
+ eslint-utils "^1.3.0"
+ regexpp "^2.0.1"
+
+eslint-plugin-import@~2.18.0:
+ version "2.18.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.0.tgz#7a5ba8d32622fb35eb9c8db195c2090bd18a3678"
+ integrity sha512-PZpAEC4gj/6DEMMoU2Df01C5c50r7zdGIN52Yfi7CvvWaYssG7Jt5R9nFG5gmqodxNOz9vQS87xk6Izdtpdrig==
+ dependencies:
+ array-includes "^3.0.3"
+ contains-path "^0.1.0"
+ debug "^2.6.9"
+ doctrine "1.5.0"
+ eslint-import-resolver-node "^0.3.2"
+ eslint-module-utils "^2.4.0"
+ has "^1.0.3"
+ lodash "^4.17.11"
+ minimatch "^3.0.4"
+ read-pkg-up "^2.0.0"
+ resolve "^1.11.0"
+
+eslint-plugin-jest@~22.7.1:
+ version "22.7.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.1.tgz#5dcdf8f7a285f98040378220d6beca581f0ab2a1"
+ integrity sha512-CrT3AzA738neimv8G8iK2HCkrCwHnAJeeo7k5TEHK86VMItKl6zdJT/tHBDImfnVVAYsVs4Y6BUdBZQCCgfiyw==
+
+eslint-plugin-node@~9.1.0:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a"
+ integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw==
+ dependencies:
+ eslint-plugin-es "^1.4.0"
+ eslint-utils "^1.3.1"
+ ignore "^5.1.1"
+ minimatch "^3.0.4"
+ resolve "^1.10.1"
+ semver "^6.1.0"
+
+eslint-plugin-prettier@~3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.0.tgz#8695188f95daa93b0dc54b249347ca3b79c4686d"
+ integrity sha512-XWX2yVuwVNLOUhQijAkXz+rMPPoCr7WFiAl8ig6I7Xn+pPVhDhzg4DxHpmbeb0iqjO9UronEA3Tb09ChnFVHHA==
+ dependencies:
+ prettier-linter-helpers "^1.0.0"
+
+eslint-plugin-promise@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
+ integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==
+
+eslint-plugin-standard@~4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.0.tgz#f845b45109c99cd90e77796940a344546c8f6b5c"
+ integrity sha512-OwxJkR6TQiYMmt1EsNRMe5qG3GsbjlcOhbGUBY4LtavF9DsLaTcoR+j2Tdjqi23oUwKNUqX7qcn5fPStafMdlA==
+
+eslint-scope@3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+ integrity sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-scope@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
+ integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-utils@^1.3.0, eslint-utils@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512"
+ integrity sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==
+
+eslint-visitor-keys@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
+ integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
+
+eslint@~6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.0.1.tgz#4a32181d72cb999d6f54151df7d337131f81cda7"
+ integrity sha512-DyQRaMmORQ+JsWShYsSg4OPTjY56u1nCjAmICrE8vLWqyLKxhFXOthwMj1SA8xwfrv0CofLNVnqbfyhwCkaO0w==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ ajv "^6.10.0"
+ chalk "^2.1.0"
+ cross-spawn "^6.0.5"
+ debug "^4.0.1"
+ doctrine "^3.0.0"
+ eslint-scope "^4.0.3"
+ eslint-utils "^1.3.1"
+ eslint-visitor-keys "^1.0.0"
+ espree "^6.0.0"
+ esquery "^1.0.1"
+ esutils "^2.0.2"
+ file-entry-cache "^5.0.1"
+ functional-red-black-tree "^1.0.1"
+ glob-parent "^3.1.0"
+ globals "^11.7.0"
+ ignore "^4.0.6"
+ import-fresh "^3.0.0"
+ imurmurhash "^0.1.4"
+ inquirer "^6.2.2"
+ is-glob "^4.0.0"
+ js-yaml "^3.13.1"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ levn "^0.3.0"
+ lodash "^4.17.11"
+ minimatch "^3.0.4"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ optionator "^0.8.2"
+ progress "^2.0.0"
+ regexpp "^2.0.1"
+ semver "^5.5.1"
+ strip-ansi "^4.0.0"
+ strip-json-comments "^2.0.1"
+ table "^5.2.3"
+ text-table "^0.2.0"
+
+espree@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-6.0.0.tgz#716fc1f5a245ef5b9a7fdb1d7b0d3f02322e75f6"
+ integrity sha512-lJvCS6YbCn3ImT3yKkPe0+tJ+mH6ljhGNjHQH9mRtiO6gjhVAOhVXW1yjnwqGwTkK3bGbye+hb00nFNmu0l/1Q==
+ dependencies:
+ acorn "^6.0.7"
+ acorn-jsx "^5.0.0"
+ eslint-visitor-keys "^1.0.0"
+
+esprima@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+ integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
+
+esprima@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+esquery@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
+ integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==
+ dependencies:
+ estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
+ integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==
+ dependencies:
+ estraverse "^4.1.0"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+ integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+ integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
+
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+ integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+
+eventemitter3@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
+ integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==
+
+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"
+ integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg==
+
+execa@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
+ integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=
+ dependencies:
+ cross-spawn "^5.0.1"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^4.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+exit@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+ integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=
+
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+expect-ct@0.2.0:
+ version "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@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-24.8.0.tgz#471f8ec256b7b6129ca2524b2a62f030df38718d"
+ integrity sha512-/zYvP8iMDrzaaxHVa724eJBCKqSHmO0FA7EDkBiRHxg6OipmMn1fN+C8T9L9K8yr7UONkOifu6+LLH+z76CnaA==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ ansi-styles "^3.2.0"
+ jest-get-type "^24.8.0"
+ jest-matcher-utils "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-regex-util "^24.3.0"
+
+express@^4.0.0, express@^4.16.3, express@~4.17.1:
+ version "4.17.1"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
+ integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
+ dependencies:
+ accepts "~1.3.7"
+ array-flatten "1.1.1"
+ body-parser "1.19.0"
+ content-disposition "0.5.3"
+ content-type "~1.0.4"
+ cookie "0.4.0"
+ cookie-signature "1.0.6"
+ debug "2.6.9"
+ depd "~1.1.2"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ finalhandler "~1.1.2"
+ fresh "0.5.2"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "~2.3.0"
+ parseurl "~1.3.3"
+ path-to-regexp "0.1.7"
+ proxy-addr "~2.0.5"
+ qs "6.7.0"
+ range-parser "~1.2.1"
+ safe-buffer "5.1.2"
+ send "0.17.1"
+ serve-static "1.14.1"
+ setprototypeof "1.1.1"
+ statuses "~1.5.0"
+ type-is "~1.6.18"
+ utils-merge "1.0.1"
+ vary "~1.1.2"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
+extend@^3.0.0, extend@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+extendable-error@^0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/extendable-error/-/extendable-error-0.1.5.tgz#122308a7097bc89a263b2c4fbf089c78140e3b6d"
+ integrity sha1-EiMIpwl7yJomOyxPvwiceBQOO20=
+
+external-editor@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27"
+ integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==
+ dependencies:
+ chardet "^0.7.0"
+ iconv-lite "^0.4.24"
+ tmp "^0.0.33"
+
+extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+ integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+ integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+faker@Marak/faker.js#master:
+ version "4.1.0"
+ resolved "https://codeload.github.com/Marak/faker.js/tar.gz/10bfb9f467b0ac2b8912ffc15690b50ef3244f09"
+
+fast-deep-equal@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+ integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
+
+fast-diff@^1.1.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
+ integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+ integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+ integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+fb-watchman@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+ integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=
+ dependencies:
+ bser "^2.0.0"
+
+feature-policy@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/feature-policy/-/feature-policy-0.3.0.tgz#7430e8e54a40da01156ca30aaec1a381ce536069"
+ integrity sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ==
+
+figures@2.0.0, figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
+ integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+file-entry-cache@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
+ integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
+ dependencies:
+ flat-cache "^2.0.1"
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
+finalhandler@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
+ integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.3"
+ statuses "~1.5.0"
+ unpipe "~1.0.0"
+
+find-cache-dir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d"
+ integrity sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA==
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^1.0.0"
+ pkg-dir "^3.0.0"
+
+find-up@^2.0.0, find-up@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
+ dependencies:
+ locate-path "^2.0.0"
+
+find-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+ integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+ dependencies:
+ locate-path "^3.0.0"
+
+flat-cache@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
+ integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
+ dependencies:
+ flatted "^2.0.0"
+ rimraf "2.6.3"
+ write "1.0.3"
+
+flatted@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916"
+ integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==
+
+fn-name@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7"
+ integrity sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=
+
+for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+ integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+ integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@^2.3.1, form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.6"
+ mime-types "^2.1.12"
+
+formidable@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659"
+ integrity sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==
+
+forwarded@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
+ integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+ dependencies:
+ map-cache "^0.2.2"
+
+frameguard@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/frameguard/-/frameguard-3.1.0.tgz#bd1442cca1d67dc346a6751559b6d04502103a22"
+ integrity sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g==
+
+fresh@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+ integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+
+fs-capacitor@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-1.0.1.tgz#ff9dbfa14dfaf4472537720f19c3088ed9278df0"
+ integrity sha512-XdZK0Q78WP29Vm3FGgJRhRhrBm51PagovzWtW2kJ3Q6cYJbGtZqWSGTSPwvtEkyjIirFd7b8Yes/dpOYjt4RRQ==
+
+fs-capacitor@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c"
+ integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==
+
+fs-minipass@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
+ integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==
+ dependencies:
+ minipass "^2.2.1"
+
+fs-readdir-recursive@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27"
+ integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4"
+ integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==
+ dependencies:
+ nan "^2.9.2"
+ node-pre-gyp "^0.10.0"
+
+function-bind@^1.0.2, function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+functional-red-black-tree@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+ integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
+ integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
+
+get-func-name@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
+ integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=
+
+get-stdin@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
+ integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==
+
+get-stream@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+ integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
+
+get-stream@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+ integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+ dependencies:
+ pump "^3.0.0"
+
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+ integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+ dependencies:
+ assert-plus "^1.0.0"
+
+gherkin@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/gherkin/-/gherkin-5.1.0.tgz#684bbb03add24eaf7bdf544f58033eb28fb3c6d5"
+ integrity sha1-aEu7A63STq9731RPWAM+so+zxtU=
+
+glob-parent@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+ integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
+ dependencies:
+ is-glob "^3.1.0"
+ path-dirname "^1.0.0"
+
+glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
+ integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+global-dirs@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
+ integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=
+ dependencies:
+ ini "^1.3.4"
+
+globals@^11.1.0, globals@^11.7.0:
+ version "11.9.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.9.0.tgz#bde236808e987f290768a93d065060d78e6ab249"
+ integrity sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==
+
+got@^6.7.1:
+ version "6.7.1"
+ resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
+ integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=
+ dependencies:
+ create-error-class "^3.0.0"
+ duplexer3 "^0.1.4"
+ get-stream "^3.0.0"
+ is-redirect "^1.0.0"
+ is-retry-allowed "^1.0.0"
+ is-stream "^1.0.0"
+ lowercase-keys "^1.0.0"
+ safe-buffer "^5.0.1"
+ timed-out "^4.0.0"
+ unzip-response "^2.0.1"
+ url-parse-lax "^1.0.0"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
+ version "4.1.15"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
+ integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
+
+"graceful-readlink@>= 1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+ integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
+
+graphql-auth-directives@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/graphql-auth-directives/-/graphql-auth-directives-2.1.0.tgz#85b83817844e2ec5fba8fe5de444287d6dd0f85a"
+ integrity sha512-mRVsjeMeMABPyjxyzl9mhkcW02YBwSj7dnu7C6wy2dIhiby6xTKy6Q54C8KeqXSYsy6ua4VmBH++d7GKqpvIoA==
+ dependencies:
+ apollo-errors "^1.9.0"
+ graphql-tools "^4.0.4"
+ jsonwebtoken "^8.3.0"
+
+graphql-custom-directives@~0.2.14:
+ version "0.2.14"
+ resolved "https://registry.yarnpkg.com/graphql-custom-directives/-/graphql-custom-directives-0.2.14.tgz#88611b8cb074477020ad85af47bfe168c4c23992"
+ integrity sha512-c3+r+st7dbBNGOLumkWrnv4nwAHJr1sZnkYc72AIMtzjuQ4+Slk1ZsFVYt1kwXJpfxXgf6g2g0jYc9+Lmz4ENg==
+ dependencies:
+ libphonenumber-js "^1.6.4"
+ lodash "^4.17.5"
+ moment "^2.22.2"
+ numeral "^2.0.6"
+
+graphql-deduplicator@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/graphql-deduplicator/-/graphql-deduplicator-2.0.2.tgz#d8608161cf6be97725e178df0c41f6a1f9f778f3"
+ integrity sha512-0CGmTmQh4UvJfsaTPppJAcHwHln8Ayat7yXXxdnuWT+Mb1dBzkbErabCWzjXyKh/RefqlGTTA7EQOZHofMaKJA==
+
+graphql-extensions@0.7.4:
+ version "0.7.4"
+ resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.4.tgz#78327712822281d5778b9210a55dc59c93a9c184"
+ integrity sha512-Ly+DiTDU+UtlfPGQkqmBX2SWMr9OT3JxMRwpB9K86rDNDBTJtG6AE2kliQKKE+hg1+945KAimO7Ep+YAvS7ywg==
+ dependencies:
+ "@apollographql/apollo-tools" "^0.3.6"
+
+graphql-extensions@0.7.6:
+ version "0.7.6"
+ resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.6.tgz#80cdddf08b0af12525529d1922ee2ea0d0cc8ecf"
+ integrity sha512-RV00O3YFD1diehvdja180BlKOGWgeigr/8/Wzr6lXwLcFtk6FecQC/7nf6oW1qhuXczHyNjt/uCr0WWbWq6mYg==
+ dependencies:
+ "@apollographql/apollo-tools" "^0.3.6"
+
+graphql-extensions@^0.0.x, graphql-extensions@~0.0.9:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.0.10.tgz#34bdb2546d43f6a5bc89ab23c295ec0466c6843d"
+ integrity sha512-TnQueqUDCYzOSrpQb3q1ngDSP2otJSF+9yNLrQGPzkMsvnQ+v6e2d5tl+B35D4y+XpmvVnAn4T3ZK28mkILveA==
+ dependencies:
+ core-js "^2.5.3"
+ source-map-support "^0.5.1"
+
+graphql-import@^0.7.0:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/graphql-import/-/graphql-import-0.7.1.tgz#4add8d91a5f752d764b0a4a7a461fcd93136f223"
+ integrity sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==
+ dependencies:
+ lodash "^4.17.4"
+ resolve-from "^4.0.0"
+
+graphql-iso-date@~3.6.1:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/graphql-iso-date/-/graphql-iso-date-3.6.1.tgz#bd2d0dc886e0f954cbbbc496bbf1d480b57ffa96"
+ integrity sha512-AwFGIuYMJQXOEAgRlJlFL4H1ncFM8n8XmoVDTNypNOZyQ8LFDG2ppMFlsS862BSTCDcSUfHp8PD3/uJhv7t59Q==
+
+graphql-middleware@3.0.2, graphql-middleware@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/graphql-middleware/-/graphql-middleware-3.0.2.tgz#c8cdb67615eec02aec237b455e679f5fc973ddc4"
+ integrity sha512-sRqu1sF+77z42z1OVM1QDHKQWnWY5K3nAgqWiZwx3U4tqNZprrDuXxSChPMliV343IrVkpYdejUYq9w24Ot3FA==
+ dependencies:
+ graphql-tools "^4.0.4"
+
+graphql-playground-html@1.6.12:
+ version "1.6.12"
+ resolved "https://registry.yarnpkg.com/graphql-playground-html/-/graphql-playground-html-1.6.12.tgz#8b3b34ab6013e2c877f0ceaae478fafc8ca91b85"
+ integrity sha512-yOYFwwSMBL0MwufeL8bkrNDgRE7eF/kTHiwrqn9FiR9KLcNIl1xw9l9a+6yIRZM56JReQOHpbQFXTZn1IuSKRg==
+
+graphql-playground-middleware-express@1.7.11:
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/graphql-playground-middleware-express/-/graphql-playground-middleware-express-1.7.11.tgz#bbffd784a37133bfa7165bdd8f429081dbf4bcf6"
+ integrity sha512-sKItB4s3FxqlwCgXdMfwRAfssSoo31bcFsGAAg/HzaZLicY6CDlofKXP8G5iPDerB6NaoAcAaBLutLzl9sd4fQ==
+ dependencies:
+ graphql-playground-html "1.6.12"
+
+graphql-playground-middleware-lambda@1.7.12:
+ version "1.7.12"
+ resolved "https://registry.yarnpkg.com/graphql-playground-middleware-lambda/-/graphql-playground-middleware-lambda-1.7.12.tgz#1b06440a288dbcd53f935b43e5b9ca2738a06305"
+ integrity sha512-fJ1Y0Ck5ctmfaQFoWv7vNnVP7We19P3miVmOT85YPrjpzbMYv0wPfxm4Zjt8nnqXr0KU9nGW53tz3K7/Lvzxtw==
+ dependencies:
+ graphql-playground-html "1.6.12"
+
+graphql-request@~1.8.2:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-1.8.2.tgz#398d10ae15c585676741bde3fc01d5ca948f8fbe"
+ integrity sha512-dDX2M+VMsxXFCmUX0Vo0TopIZIX4ggzOtiCsThgtrKR4niiaagsGTDIHj3fsOMFETpa064vzovI+4YV4QnMbcg==
+ dependencies:
+ cross-fetch "2.2.2"
+
+graphql-shield@~6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-6.0.2.tgz#3ebad8faacbada91b8e576029732e91b5a041c7f"
+ integrity sha512-3qV2qjeNZla1Fyg6Q2NR5J9AsMaNePLbUboOwhRXB7IcMnTnrxSiVn2R//8VnjnmBjF9rcvgAIAvETZ8AKGfsg==
+ dependencies:
+ "@types/yup" "0.26.20"
+ lightercollective "^0.3.0"
+ object-hash "^1.3.1"
+ yup "^0.27.0"
+
+graphql-subscriptions@^0.5.8:
+ version "0.5.8"
+ resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.5.8.tgz#13a6143c546bce390404657dc73ca501def30aa7"
+ integrity sha512-0CaZnXKBw2pwnIbvmVckby5Ge5e2ecmjofhYCdyeACbCly2j3WXDP/pl+s+Dqd2GQFC7y99NB+53jrt55CKxYQ==
+ dependencies:
+ iterall "^1.2.1"
+
+graphql-subscriptions@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.0.0.tgz#475267694b3bd465af6477dbab4263a3f62702b8"
+ integrity sha512-+ytmryoHF1LVf58NKEaNPRUzYyXplm120ntxfPcgOBC7TnK7Tv/4VRHeh4FAR9iL+O1bqhZs4nkibxQ+OA5cDQ==
+ dependencies:
+ iterall "^1.2.1"
+
+graphql-tag@^2.9.2, graphql-tag@~2.10.1:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02"
+ integrity sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg==
+
+graphql-tools@^4.0.0, graphql-tools@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.4.tgz#ca08a63454221fdde825fe45fbd315eb2a6d566b"
+ integrity sha512-chF12etTIGVVGy3fCTJ1ivJX2KB7OSG4c6UOJQuqOHCmBQwTyNgCDuejZKvpYxNZiEx7bwIjrodDgDe9RIkjlw==
+ dependencies:
+ apollo-link "^1.2.3"
+ apollo-utilities "^1.0.1"
+ deprecated-decorator "^0.1.6"
+ iterall "^1.1.3"
+ uuid "^3.1.0"
+
+graphql-upload@^8.0.0, graphql-upload@^8.0.2:
+ version "8.0.7"
+ resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.7.tgz#8644264e241529552ea4b3797e7ee15809cf01a3"
+ integrity sha512-gi2yygbDPXbHPC7H0PNPqP++VKSoNoJO4UrXWq4T0Bi4IhyUd3Ycop/FSxhx2svWIK3jdXR/i0vi91yR1aAF0g==
+ dependencies:
+ busboy "^0.3.1"
+ fs-capacitor "^2.0.4"
+ http-errors "^1.7.2"
+ object-path "^0.11.4"
+
+graphql-yoga@~1.18.0:
+ version "1.18.0"
+ resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-1.18.0.tgz#2668278e94a0bd1b2ff8c60f928c4e18d62e381a"
+ integrity sha512-WEibitQA2oFTmD7XBO8/ps8DWeVpkzOzgbB3EvtM2oIpyGhPCzRZYrC7OS9MmijvRwLRXsgHImHWUm82ZrIOWA==
+ dependencies:
+ "@types/cors" "^2.8.4"
+ "@types/express" "^4.11.1"
+ "@types/graphql" "^14.0.0"
+ "@types/graphql-deduplicator" "^2.0.0"
+ "@types/zen-observable" "^0.5.3"
+ apollo-server-express "^1.3.6"
+ apollo-server-lambda "1.3.6"
+ apollo-upload-server "^7.0.0"
+ aws-lambda "^0.1.2"
+ body-parser-graphql "1.1.0"
+ cors "^2.8.4"
+ express "^4.16.3"
+ graphql "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0"
+ graphql-deduplicator "^2.0.1"
+ graphql-import "^0.7.0"
+ graphql-middleware "3.0.2"
+ graphql-playground-middleware-express "1.7.11"
+ graphql-playground-middleware-lambda "1.7.12"
+ graphql-subscriptions "^0.5.8"
+ graphql-tools "^4.0.0"
+ graphql-upload "^8.0.0"
+ subscriptions-transport-ws "^0.9.8"
+
+"graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", graphql@^14.2.1, graphql@~14.4.0:
+ version "14.4.0"
+ resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.0.tgz#e97086acfc0338e4fdc8f7dba519c6b8a6badfd9"
+ integrity sha512-E55z1oK6e4cGxCqlSsRWytYDPcIUxky3XkbuQUf6TIjCmn6C7CuBJpmkMF1066q95yPAGOZVPTVT7jABKbRFSA==
+ dependencies:
+ iterall "^1.2.2"
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+ integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
+
+handlebars@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.0.tgz#0d6a6f34ff1f63cecec8423aa4169827bf787c3a"
+ integrity sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==
+ dependencies:
+ async "^2.5.0"
+ optimist "^0.6.1"
+ source-map "^0.6.1"
+ optionalDependencies:
+ uglify-js "^3.1.4"
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+ integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.0:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+ integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+ dependencies:
+ ajv "^6.5.5"
+ har-schema "^2.0.0"
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+ integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+ integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
+
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+ integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+has@^1.0.1, has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+hash.js@^1.0.0:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
+ integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.1"
+
+he@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2"
+ integrity sha1-LAX/rvkLaOhg8/0rVO9YCYknfuI=
+
+helmet-crossdomain@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.3.0.tgz#707e2df930f13ad61f76ed08e1bb51ab2b2e85fa"
+ integrity sha512-YiXhj0E35nC4Na5EPE4mTfoXMf9JTGpN4OtB4aLqShKuH9d2HNaJX5MQoglO6STVka0uMsHyG5lCut5Kzsy7Lg==
+
+helmet-csp@2.7.1:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.7.1.tgz#e8e0b5186ffd4db625cfcce523758adbfadb9dca"
+ integrity sha512-sCHwywg4daQ2mY0YYwXSZRsgcCeerUwxMwNixGA7aMLkVmPTYBl7gJoZDHOZyXkqPrtuDT3s2B1A+RLI7WxSdQ==
+ dependencies:
+ camelize "1.0.0"
+ content-security-policy-builder "2.0.0"
+ dasherize "2.0.0"
+ platform "1.3.5"
+
+helmet@~3.18.0:
+ version "3.18.0"
+ resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.18.0.tgz#37666f7c861bd1ff3015e0cdb903a43501e3da3e"
+ integrity sha512-TsKlGE5UVkV0NiQ4PllV9EVfZklPjyzcMEMjWlyI/8S6epqgRT+4s4GHVgc25x0TixsKvp3L7c91HQQt5l0+QA==
+ dependencies:
+ depd "2.0.0"
+ dns-prefetch-control "0.1.0"
+ dont-sniff-mimetype "1.0.0"
+ expect-ct "0.2.0"
+ feature-policy "0.3.0"
+ frameguard "3.1.0"
+ helmet-crossdomain "0.3.0"
+ helmet-csp "2.7.1"
+ hide-powered-by "1.0.0"
+ hpkp "2.0.0"
+ hsts "2.2.0"
+ ienoopen "1.1.0"
+ nocache "2.1.0"
+ referrer-policy "1.2.0"
+ x-xss-protection "1.1.0"
+
+hide-powered-by@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
+ integrity sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=
+
+hoek@5.x.x:
+ version "5.0.4"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.4.tgz#0f7fa270a1cafeb364a4b2ddfaa33f864e4157da"
+ integrity sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==
+
+hoek@6.x.x:
+ version "6.1.2"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.2.tgz#99e6d070561839de74ee427b61aa476bd6bddfd6"
+ integrity sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q==
+
+homedir-polyfill@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
+ integrity sha1-TCu8inWJmP7r9e1oWA921GdotLw=
+ dependencies:
+ parse-passwd "^1.0.0"
+
+hosted-git-info@^2.1.4:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
+ integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==
+
+hpkp@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/hpkp/-/hpkp-2.0.0.tgz#10e142264e76215a5d30c44ec43de64dee6d1672"
+ integrity sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=
+
+hsts@2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.2.0.tgz#09119d42f7a8587035d027dda4522366fe75d964"
+ integrity sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==
+ dependencies:
+ depd "2.0.0"
+
+html-encoding-sniffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+ integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
+ dependencies:
+ whatwg-encoding "^1.0.1"
+
+htmlparser2@^3.10.0, htmlparser2@^3.9.1:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464"
+ integrity sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==
+ dependencies:
+ domelementtype "^1.3.0"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^3.0.6"
+
+http-errors@1.7.2, http-errors@^1.7.2, http-errors@~1.7.2:
+ version "1.7.2"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
+ integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.3"
+ setprototypeof "1.1.1"
+ statuses ">= 1.5.0 < 2"
+ toidentifier "1.0.0"
+
+http-errors@^1.7.0:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.1.tgz#6a4ffe5d35188e1c39f872534690585852e1f027"
+ integrity sha512-jWEUgtZWGSMba9I1N3gc1HmvpBUaNC9vDdA46yScAdp+C5rdEuKWUBLWTQpW9FwSWSbYYs++b6SDCxf9UEJzfw==
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.3"
+ setprototypeof "1.1.0"
+ statuses ">= 1.5.0 < 2"
+ toidentifier "1.0.0"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+ieee754@1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
+ integrity sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=
+
+ieee754@^1.1.4:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b"
+ integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==
+
+ienoopen@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974"
+ integrity sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ==
+
+ignore-by-default@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
+ integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk=
+
+ignore-walk@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
+ integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==
+ dependencies:
+ minimatch "^3.0.4"
+
+ignore@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
+ integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+
+ignore@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.1.tgz#2fc6b8f518aff48fef65a7f348ed85632448e4a5"
+ integrity sha512-DWjnQIFLenVrwyRCKZT+7a7/U4Cqgar4WG8V++K3hw+lrW1hc/SIwdiGmtxKCVACmHULTuGeBbHJmbwW7/sAvA==
+
+import-fresh@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.0.0.tgz#a3d897f420cab0e671236897f75bc14b4885c390"
+ integrity sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==
+ dependencies:
+ parent-module "^1.0.0"
+ resolve-from "^4.0.0"
+
+import-lazy@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
+ integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=
+
+import-local@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
+ integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
+ dependencies:
+ pkg-dir "^3.0.0"
+ resolve-cwd "^2.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+ integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+
+indent-string@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
+ integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+inherits@2.0.1, inherits@=2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+ integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
+
+ini@^1.3.4, ini@~1.3.0:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+ integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+
+inquirer@^6.2.2:
+ version "6.2.2"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.2.tgz#46941176f65c9eb20804627149b743a218f25406"
+ integrity sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==
+ dependencies:
+ ansi-escapes "^3.2.0"
+ chalk "^2.4.2"
+ cli-cursor "^2.1.0"
+ cli-width "^2.0.0"
+ external-editor "^3.0.3"
+ figures "^2.0.0"
+ lodash "^4.17.11"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rxjs "^6.4.0"
+ string-width "^2.1.0"
+ strip-ansi "^5.0.0"
+ through "^2.3.6"
+
+insane@2.6.1:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/insane/-/insane-2.6.1.tgz#c7dcae7b51c20346883b71078fad6ce0483c198f"
+ integrity sha1-x9yue1HCA0aIO3EHj61s4Eg8GY8=
+ dependencies:
+ assignment "2.0.0"
+ he "0.5.0"
+
+invariant@^2.2.2, invariant@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
+ integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
+
+ipaddr.js@1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
+ integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
+
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+ dependencies:
+ kind-of "^6.0.0"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+ integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=
+ dependencies:
+ binary-extensions "^1.0.0"
+
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74=
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-callable@^1.1.3, is-callable@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
+ integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==
+
+is-ci@^1.0.10:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
+ integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==
+ dependencies:
+ ci-info "^1.5.0"
+
+is-ci@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
+ integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
+ dependencies:
+ ci-info "^2.0.0"
+
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+ dependencies:
+ kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+ integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=
+
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+ integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+ integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-generator-fn@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.0.0.tgz#038c31b774709641bda678b1f06a4e3227c10b3e"
+ integrity sha512-elzyIdM7iKoFHzcrndIqjYomImhxrFRnGP3galODoII4TB9gI7mZ+FnlLQmmjf27SxHS2gKEeyhX5/+YRS6H9g==
+
+is-generator@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-generator/-/is-generator-1.0.3.tgz#c14c21057ed36e328db80347966c693f886389f3"
+ integrity sha1-wUwhBX7TbjKNuANHlmxpP4hjifM=
+
+is-glob@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+ integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
+ dependencies:
+ is-extglob "^2.1.0"
+
+is-glob@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
+ integrity sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-installed-globally@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
+ integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=
+ dependencies:
+ global-dirs "^0.1.0"
+ is-path-inside "^1.0.0"
+
+is-npm@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
+ integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ=
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-obj@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
+ integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
+
+is-path-inside@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
+ integrity sha1-jvW33lBDej/cprToZe96pVy0gDY=
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-plain-obj@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+ integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
+
+is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
+is-promise@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+ integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
+
+is-redirect@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
+ integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=
+
+is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=
+ dependencies:
+ has "^1.0.1"
+
+is-retry-allowed@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
+ integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=
+
+is-stream@^1.0.0, is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+ integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+
+is-symbol@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
+ integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==
+ dependencies:
+ has-symbols "^1.0.0"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+ integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-windows@^1.0.0, is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+ integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+isarray@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+ integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isemail@3.x.x:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.2.0.tgz#59310a021931a9fb06bbb51e155ce0b3f236832c"
+ integrity sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==
+ dependencies:
+ punycode "2.x.x"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+ integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#0b891e5ad42312c2b9488554f603795f9a2211ba"
+ integrity sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw==
+
+istanbul-lib-instrument@^3.0.0, istanbul-lib-instrument@^3.0.1:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.1.0.tgz#a2b5484a7d445f1f311e93190813fa56dfb62971"
+ integrity sha512-ooVllVGT38HIk8MxDj/OIHXSYvH+1tq/Vb38s8ixt9GoJadXska4WkGY+0wkmtYCZNYtaARniH/DixUGGLZ0uA==
+ dependencies:
+ "@babel/generator" "^7.0.0"
+ "@babel/parser" "^7.0.0"
+ "@babel/template" "^7.0.0"
+ "@babel/traverse" "^7.0.0"
+ "@babel/types" "^7.0.0"
+ istanbul-lib-coverage "^2.0.3"
+ semver "^5.5.0"
+
+istanbul-lib-report@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.4.tgz#bfd324ee0c04f59119cb4f07dab157d09f24d7e4"
+ integrity sha512-sOiLZLAWpA0+3b5w5/dq0cjm2rrNdAfHWaGhmn7XEFW6X++IV9Ohn+pnELAl9K3rfpaeBfbmH9JU5sejacdLeA==
+ dependencies:
+ istanbul-lib-coverage "^2.0.3"
+ make-dir "^1.3.0"
+ supports-color "^6.0.0"
+
+istanbul-lib-source-maps@^3.0.1:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.2.tgz#f1e817229a9146e8424a28e5d69ba220fda34156"
+ integrity sha512-JX4v0CiKTGp9fZPmoxpu9YEkPbEqCqBbO3403VabKjH+NRXo72HafD5UgnjTEqHL2SAjaZK1XDuDOkn6I5QVfQ==
+ dependencies:
+ debug "^4.1.1"
+ istanbul-lib-coverage "^2.0.3"
+ make-dir "^1.3.0"
+ rimraf "^2.6.2"
+ source-map "^0.6.1"
+
+istanbul-reports@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.1.1.tgz#72ef16b4ecb9a4a7bd0e2001e00f95d1eec8afa9"
+ integrity sha512-FzNahnidyEPBCI0HcufJoSEoKykesRlFcSzQqjH9x0+LC8tnnE/p/90PBLu8iZTxr8yYZNyTtiAujUqyN+CIxw==
+ dependencies:
+ handlebars "^4.1.0"
+
+iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7"
+ integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==
+
+jest-changed-files@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.8.0.tgz#7e7eb21cf687587a85e50f3d249d1327e15b157b"
+ integrity sha512-qgANC1Yrivsq+UrLXsvJefBKVoCsKB0Hv+mBb6NMjjZ90wwxCDmU3hsCXBya30cH+LnPYjwgcU65i6yJ5Nfuug==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ execa "^1.0.0"
+ throat "^4.0.0"
+
+jest-cli@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.8.0.tgz#b075ac914492ed114fa338ade7362a301693e989"
+ integrity sha512-+p6J00jSMPQ116ZLlHJJvdf8wbjNbZdeSX9ptfHX06/MSNaXmKihQzx5vQcw0q2G6JsdVkUIdWbOWtSnaYs3yA==
+ dependencies:
+ "@jest/core" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ import-local "^2.0.0"
+ is-ci "^2.0.0"
+ jest-config "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ prompts "^2.0.1"
+ realpath-native "^1.1.0"
+ yargs "^12.0.2"
+
+jest-config@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.8.0.tgz#77db3d265a6f726294687cbbccc36f8a76ee0f4f"
+ integrity sha512-Czl3Nn2uEzVGsOeaewGWoDPD8GStxCpAe0zOYs2x2l0fZAgPbCr3uwUkgNKV3LwE13VXythM946cd5rdGkkBZw==
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/test-sequencer" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ babel-jest "^24.8.0"
+ chalk "^2.0.1"
+ glob "^7.1.1"
+ jest-environment-jsdom "^24.8.0"
+ jest-environment-node "^24.8.0"
+ jest-get-type "^24.8.0"
+ jest-jasmine2 "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ micromatch "^3.1.10"
+ pretty-format "^24.8.0"
+ realpath-native "^1.1.0"
+
+jest-diff@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.8.0.tgz#146435e7d1e3ffdf293d53ff97e193f1d1546172"
+ integrity sha512-wxetCEl49zUpJ/bvUmIFjd/o52J+yWcoc5ZyPq4/W1LUKGEhRYDIbP1KcF6t+PvqNrGAFk4/JhtxDq/Nnzs66g==
+ dependencies:
+ chalk "^2.0.1"
+ diff-sequences "^24.3.0"
+ jest-get-type "^24.8.0"
+ pretty-format "^24.8.0"
+
+jest-docblock@^24.3.0:
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.3.0.tgz#b9c32dac70f72e4464520d2ba4aec02ab14db5dd"
+ integrity sha512-nlANmF9Yq1dufhFlKG9rasfQlrY7wINJbo3q01tu56Jv5eBU5jirylhF2O5ZBnLxzOVBGRDz/9NAwNyBtG4Nyg==
+ dependencies:
+ detect-newline "^2.1.0"
+
+jest-each@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.8.0.tgz#a05fd2bf94ddc0b1da66c6d13ec2457f35e52775"
+ integrity sha512-NrwK9gaL5+XgrgoCsd9svsoWdVkK4gnvyhcpzd6m487tXHqIdYeykgq3MKI1u4I+5Zf0tofr70at9dWJDeb+BA==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ jest-get-type "^24.8.0"
+ jest-util "^24.8.0"
+ pretty-format "^24.8.0"
+
+jest-environment-jsdom@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.8.0.tgz#300f6949a146cabe1c9357ad9e9ecf9f43f38857"
+ integrity sha512-qbvgLmR7PpwjoFjM/sbuqHJt/NCkviuq9vus9NBn/76hhSidO+Z6Bn9tU8friecegbJL8gzZQEMZBQlFWDCwAQ==
+ dependencies:
+ "@jest/environment" "^24.8.0"
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ jest-mock "^24.8.0"
+ jest-util "^24.8.0"
+ jsdom "^11.5.1"
+
+jest-environment-node@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.8.0.tgz#d3f726ba8bc53087a60e7a84ca08883a4c892231"
+ integrity sha512-vIGUEScd1cdDgR6sqn2M08sJTRLQp6Dk/eIkCeO4PFHxZMOgy+uYLPMC4ix3PEfM5Au/x3uQ/5Tl0DpXXZsJ/Q==
+ dependencies:
+ "@jest/environment" "^24.8.0"
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ jest-mock "^24.8.0"
+ jest-util "^24.8.0"
+
+jest-get-type@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.8.0.tgz#a7440de30b651f5a70ea3ed7ff073a32dfe646fc"
+ integrity sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ==
+
+jest-haste-map@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.8.0.tgz#51794182d877b3ddfd6e6d23920e3fe72f305800"
+ integrity sha512-ZBPRGHdPt1rHajWelXdqygIDpJx8u3xOoLyUBWRW28r3tagrgoepPrzAozW7kW9HrQfhvmiv1tncsxqHJO1onQ==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ anymatch "^2.0.0"
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.1.15"
+ invariant "^2.2.4"
+ jest-serializer "^24.4.0"
+ jest-util "^24.8.0"
+ jest-worker "^24.6.0"
+ micromatch "^3.1.10"
+ sane "^4.0.3"
+ walker "^1.0.7"
+ optionalDependencies:
+ fsevents "^1.2.7"
+
+jest-jasmine2@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz#a9c7e14c83dd77d8b15e820549ce8987cc8cd898"
+ integrity sha512-cEky88npEE5LKd5jPpTdDCLvKkdyklnaRycBXL6GNmpxe41F0WN44+i7lpQKa/hcbXaQ+rc9RMaM4dsebrYong==
+ dependencies:
+ "@babel/traverse" "^7.1.0"
+ "@jest/environment" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ co "^4.6.0"
+ expect "^24.8.0"
+ is-generator-fn "^2.0.0"
+ jest-each "^24.8.0"
+ jest-matcher-utils "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-snapshot "^24.8.0"
+ jest-util "^24.8.0"
+ pretty-format "^24.8.0"
+ throat "^4.0.0"
+
+jest-leak-detector@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.8.0.tgz#c0086384e1f650c2d8348095df769f29b48e6980"
+ integrity sha512-cG0yRSK8A831LN8lIHxI3AblB40uhv0z+SsQdW3GoMMVcK+sJwrIIyax5tu3eHHNJ8Fu6IMDpnLda2jhn2pD/g==
+ dependencies:
+ pretty-format "^24.8.0"
+
+jest-matcher-utils@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.8.0.tgz#2bce42204c9af12bde46f83dc839efe8be832495"
+ integrity sha512-lex1yASY51FvUuHgm0GOVj7DCYEouWSlIYmCW7APSqB9v8mXmKSn5+sWVF0MhuASG0bnYY106/49JU1FZNl5hw==
+ dependencies:
+ chalk "^2.0.1"
+ jest-diff "^24.8.0"
+ jest-get-type "^24.8.0"
+ pretty-format "^24.8.0"
+
+jest-message-util@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.8.0.tgz#0d6891e72a4beacc0292b638685df42e28d6218b"
+ integrity sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/stack-utils" "^1.0.1"
+ chalk "^2.0.1"
+ micromatch "^3.1.10"
+ slash "^2.0.0"
+ stack-utils "^1.0.1"
+
+jest-mock@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.8.0.tgz#2f9d14d37699e863f1febf4e4d5a33b7fdbbde56"
+ integrity sha512-6kWugwjGjJw+ZkK4mDa0Df3sDlUTsV47MSrT0nGQ0RBWJbpODDQ8MHDVtGtUYBne3IwZUhtB7elxHspU79WH3A==
+ dependencies:
+ "@jest/types" "^24.8.0"
+
+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@^24.3.0:
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.3.0.tgz#d5a65f60be1ae3e310d5214a0307581995227b36"
+ integrity sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg==
+
+jest-resolve-dependencies@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.8.0.tgz#19eec3241f2045d3f990dba331d0d7526acff8e0"
+ integrity sha512-hyK1qfIf/krV+fSNyhyJeq3elVMhK9Eijlwy+j5jqmZ9QsxwKBiP6qukQxaHtK8k6zql/KYWwCTQ+fDGTIJauw==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-snapshot "^24.8.0"
+
+jest-resolve@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.8.0.tgz#84b8e5408c1f6a11539793e2b5feb1b6e722439f"
+ integrity sha512-+hjSzi1PoRvnuOICoYd5V/KpIQmkAsfjFO71458hQ2Whi/yf1GDeBOFj8Gxw4LrApHsVJvn5fmjcPdmoUHaVKw==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ browser-resolve "^1.11.3"
+ chalk "^2.0.1"
+ jest-pnp-resolver "^1.2.1"
+ realpath-native "^1.1.0"
+
+jest-runner@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.8.0.tgz#4f9ae07b767db27b740d7deffad0cf67ccb4c5bb"
+ integrity sha512-utFqC5BaA3JmznbissSs95X1ZF+d+4WuOWwpM9+Ak356YtMhHE/GXUondZdcyAAOTBEsRGAgH/0TwLzfI9h7ow==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/environment" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.4.2"
+ exit "^0.1.2"
+ graceful-fs "^4.1.15"
+ jest-config "^24.8.0"
+ jest-docblock "^24.3.0"
+ jest-haste-map "^24.8.0"
+ jest-jasmine2 "^24.8.0"
+ jest-leak-detector "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-resolve "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-util "^24.8.0"
+ jest-worker "^24.6.0"
+ source-map-support "^0.5.6"
+ throat "^4.0.0"
+
+jest-runtime@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.8.0.tgz#05f94d5b05c21f6dc54e427cd2e4980923350620"
+ integrity sha512-Mq0aIXhvO/3bX44ccT+czU1/57IgOMyy80oM0XR/nyD5zgBcesF84BPabZi39pJVA6UXw+fY2Q1N+4BiVUBWOA==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/environment" "^24.8.0"
+ "@jest/source-map" "^24.3.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/yargs" "^12.0.2"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.3"
+ graceful-fs "^4.1.15"
+ jest-config "^24.8.0"
+ jest-haste-map "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-mock "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.8.0"
+ jest-snapshot "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ realpath-native "^1.1.0"
+ slash "^2.0.0"
+ strip-bom "^3.0.0"
+ yargs "^12.0.2"
+
+jest-serializer@^24.4.0:
+ version "24.4.0"
+ resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.4.0.tgz#f70c5918c8ea9235ccb1276d232e459080588db3"
+ integrity sha512-k//0DtglVstc1fv+GY/VHDIjrtNjdYvYjMlbLUed4kxrE92sIUewOi5Hj3vrpB8CXfkJntRPDRjCrCvUhBdL8Q==
+
+jest-snapshot@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.8.0.tgz#3bec6a59da2ff7bc7d097a853fb67f9d415cb7c6"
+ integrity sha512-5ehtWoc8oU9/cAPe6fez6QofVJLBKyqkY2+TlKTOf0VllBB/mqUNdARdcjlZrs9F1Cv+/HKoCS/BknT0+tmfPg==
+ dependencies:
+ "@babel/types" "^7.0.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ expect "^24.8.0"
+ jest-diff "^24.8.0"
+ jest-matcher-utils "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-resolve "^24.8.0"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ pretty-format "^24.8.0"
+ semver "^5.5.0"
+
+jest-util@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.8.0.tgz#41f0e945da11df44cc76d64ffb915d0716f46cd1"
+ integrity sha512-DYZeE+XyAnbNt0BG1OQqKy/4GVLPtzwGx5tsnDrFcax36rVE3lTA5fbvgmbVPUZf9w77AJ8otqR4VBbfFJkUZA==
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/source-map" "^24.3.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ callsites "^3.0.0"
+ chalk "^2.0.1"
+ graceful-fs "^4.1.15"
+ is-ci "^2.0.0"
+ mkdirp "^0.5.1"
+ slash "^2.0.0"
+ source-map "^0.6.0"
+
+jest-validate@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.8.0.tgz#624c41533e6dfe356ffadc6e2423a35c2d3b4849"
+ integrity sha512-+/N7VOEMW1Vzsrk3UWBDYTExTPwf68tavEPKDnJzrC6UlHtUDU/fuEdXqFoHzv9XnQ+zW6X3qMZhJ3YexfeLDA==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ camelcase "^5.0.0"
+ chalk "^2.0.1"
+ jest-get-type "^24.8.0"
+ leven "^2.1.0"
+ pretty-format "^24.8.0"
+
+jest-watcher@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.8.0.tgz#58d49915ceddd2de85e238f6213cef1c93715de4"
+ integrity sha512-SBjwHt5NedQoVu54M5GEx7cl7IGEFFznvd/HNT8ier7cCAx/Qgu9ZMlaTQkvK22G1YOpcWBLQPFSImmxdn3DAw==
+ dependencies:
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/yargs" "^12.0.9"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ jest-util "^24.8.0"
+ string-length "^2.0.0"
+
+jest-worker@^24.6.0:
+ version "24.6.0"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.6.0.tgz#7f81ceae34b7cde0c9827a6980c35b7cdc0161b3"
+ integrity sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ==
+ dependencies:
+ merge-stream "^1.0.1"
+ supports-color "^6.1.0"
+
+jest@~24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-24.8.0.tgz#d5dff1984d0d1002196e9b7f12f75af1b2809081"
+ integrity sha512-o0HM90RKFRNWmAWvlyV8i5jGZ97pFwkeVoGvPW1EtLTgJc2+jcuqcbbqcSZLE/3f2S5pt0y2ZBETuhpWNl1Reg==
+ dependencies:
+ import-local "^2.0.0"
+ jest-cli "^24.8.0"
+
+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=
+
+joi@^13.0.0:
+ version "13.7.0"
+ resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f"
+ integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==
+ dependencies:
+ hoek "5.x.x"
+ isemail "3.x.x"
+ topo "3.x.x"
+
+jquery@^3.3.1:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.0.tgz#8de513fa0fa4b2c7d2e48a530e26f0596936efdf"
+ integrity sha512-ggRCXln9zEqv6OqAGXFEcshF5dSBvCkzj6Gm2gzuR5fWawaX8t7cxKVkkygKODrDAzKdoYw3l/e3pm3vlT4IbQ==
+
+js-levenshtein@^1.1.3:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.4.tgz#3a56e3cbf589ca0081eb22cd9ba0b1290a16d26e"
+ integrity sha512-PxfGzSs0ztShKrUYPIn5r0MtyAhYcCwmndozzpz8YObbPnD1jFxzlBGbRnX2mIu6Z13xN6+PTu05TQFnZFlzow==
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^3.13.1:
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
+ integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+ integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsdom@^11.5.1:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8"
+ integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==
+ dependencies:
+ abab "^2.0.0"
+ acorn "^5.5.3"
+ acorn-globals "^4.1.0"
+ array-equal "^1.0.0"
+ cssom ">= 0.3.2 < 0.4.0"
+ cssstyle "^1.0.0"
+ data-urls "^1.0.0"
+ domexception "^1.0.1"
+ escodegen "^1.9.1"
+ html-encoding-sniffer "^1.0.2"
+ left-pad "^1.3.0"
+ nwsapi "^2.0.7"
+ parse5 "4.0.0"
+ pn "^1.1.0"
+ request "^2.87.0"
+ request-promise-native "^1.0.5"
+ sax "^1.2.4"
+ symbol-tree "^3.2.2"
+ tough-cookie "^2.3.4"
+ w3c-hr-time "^1.0.1"
+ webidl-conversions "^4.0.2"
+ whatwg-encoding "^1.0.3"
+ whatwg-mimetype "^2.1.0"
+ whatwg-url "^6.4.1"
+ ws "^5.2.0"
+ xml-name-validator "^3.0.0"
+
+jsesc@^2.5.1:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+ integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+ integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
+
+json-parse-better-errors@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+ integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+ integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+json-stable-stringify-without-jsonify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+ integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
+
+json-stringify-safe@~5.0.1:
+ version "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==
+ dependencies:
+ minimist "^1.2.0"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+ integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
+
+jsonld-signatures@^1.1.5:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/jsonld-signatures/-/jsonld-signatures-1.2.1.tgz#493df5df9cd3a9f1b1cb296bbd3d081679f20ca8"
+ integrity sha1-ST3135zTqfGxyylrvT0IFnnyDKg=
+ dependencies:
+ async "^1.5.2"
+ bitcore-message "github:CoMakery/bitcore-message#dist"
+ commander "~2.9.0"
+ es6-promise "~4.0.5"
+ jsonld "0.4.3"
+ node-forge "~0.6.45"
+
+jsonld@0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.4.3.tgz#0bbc929190064d6650a5af5876e1bfdf0ed288f3"
+ integrity sha1-C7ySkZAGTWZQpa9YduG/3w7SiPM=
+ dependencies:
+ es6-promise "~2.0.1"
+ pkginfo "~0.3.0"
+ request "^2.61.0"
+ xmldom "0.1.19"
+
+jsonld@^0.4.11:
+ version "0.4.12"
+ resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.4.12.tgz#a02f205d5341414df1b6d8414f1b967a712073e8"
+ integrity sha1-oC8gXVNBQU3xtthBTxuWenEgc+g=
+ dependencies:
+ es6-promise "^2.0.0"
+ pkginfo "~0.4.0"
+ request "^2.61.0"
+ xmldom "0.1.19"
+
+jsonwebtoken@^8.3.0, jsonwebtoken@~8.5.1:
+ version "8.5.1"
+ resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
+ integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
+ dependencies:
+ jws "^3.2.2"
+ lodash.includes "^4.3.0"
+ lodash.isboolean "^3.0.3"
+ lodash.isinteger "^4.0.4"
+ lodash.isnumber "^3.0.3"
+ lodash.isplainobject "^4.0.6"
+ lodash.isstring "^4.0.1"
+ lodash.once "^4.0.0"
+ ms "^2.1.1"
+ semver "^5.6.0"
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+jwa@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
+ integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
+ dependencies:
+ buffer-equal-constant-time "1.0.1"
+ ecdsa-sig-formatter "1.0.11"
+ safe-buffer "^5.0.1"
+
+jws@^3.2.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
+ integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
+ dependencies:
+ jwa "^1.4.1"
+ safe-buffer "^5.0.1"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+ integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+ integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
+
+kleur@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.1.tgz#4f5b313f5fa315432a400f19a24db78d451ede62"
+ integrity sha512-P3kRv+B+Ra070ng2VKQqW4qW7gd/v3iD8sy/zOdcYRsfiD+QBokQNOps/AfP6Hr48cBhIIBFWckB9aO+IZhrWg==
+
+knuth-shuffle-seeded@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz#01f1b65733aa7540ee08d8b0174164d22081e4e1"
+ integrity sha1-AfG2VzOqdUDuCNiwF0Fk0iCB5OE=
+ dependencies:
+ seed-random "~2.2.0"
+
+latest-version@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
+ integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=
+ dependencies:
+ package-json "^4.0.0"
+
+lcid@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
+ integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==
+ dependencies:
+ invert-kv "^2.0.0"
+
+left-pad@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
+ integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
+
+leven@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
+ integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA=
+
+levn@^0.3.0, levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+libphonenumber-js@^1.6.4:
+ version "1.6.9"
+ resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.6.9.tgz#3ac541f29778d8096bef5bcb3e319dd4088ce32e"
+ integrity sha512-PxN3pUzLIhamXd3WOZp0TwPeaJhU30OViP7cLeG9FRz1X68Az9ARHCwN5ydVvrfEC7ifbvNwB72dM+S1IFYiZw==
+ dependencies:
+ minimist "^1.2.0"
+ semver-compare "^1.0.0"
+ xml2js "^0.4.17"
+
+lightercollective@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/lightercollective/-/lightercollective-0.3.0.tgz#1f07638642ec645d70bdb69ab2777676f35a28f0"
+ integrity sha512-RFOLSUVvwdK3xA0P8o6G7QGXLIyy1L2qv5caEI7zXN5ciaEjbAriRF182kbsoJ1S1TgvpyGcN485fMky6qxOPw==
+
+linkifyjs@~2.1.8:
+ version "2.1.8"
+ resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-2.1.8.tgz#2bee2272674dc196cce3740b8436c43df2162f9c"
+ integrity sha512-j3QpiEr4UYzN5foKhrr9Sr06VI9vSlI4HisDWt+7Mq+TWDwpJ6H/LLpogYsXcyUIJLVhGblXXdUnblHsVNMPpg==
+ optionalDependencies:
+ jquery "^3.3.1"
+ react "^16.4.2"
+ react-dom "^16.4.2"
+
+load-json-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+ integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
+load-json-file@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
+ integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^4.0.0"
+ pify "^3.0.0"
+ strip-bom "^3.0.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+locate-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+ integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
+lodash.clonedeep@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+ integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+
+lodash.escaperegexp@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
+ integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=
+
+lodash.includes@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
+ integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
+
+lodash.isboolean@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
+ integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
+
+lodash.isinteger@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
+ integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
+
+lodash.isnumber@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
+ integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
+
+lodash.isplainobject@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+ integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+
+lodash.isstring@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
+ integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
+
+lodash.mergewith@^4.6.1:
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
+ integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
+
+lodash.once@^4.0.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
+ integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
+
+lodash.sortby@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+ integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
+
+lodash@=3.10.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
+ integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
+
+lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.11:
+ version "4.17.11"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+ integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
+
+long@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
+ integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+lower-case@^1.1.1:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
+ integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw=
+
+lowercase-keys@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
+ integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
+
+lru-cache@^4.0.1:
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
+ integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+lru-cache@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+make-dir@^1.0.0, make-dir@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
+ integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==
+ dependencies:
+ pify "^3.0.0"
+
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=
+ dependencies:
+ tmpl "1.0.x"
+
+map-age-cleaner@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
+ integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
+ dependencies:
+ p-defer "^1.0.0"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+ integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+ dependencies:
+ object-visit "^1.0.0"
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+ integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+
+mem@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a"
+ integrity sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==
+ dependencies:
+ map-age-cleaner "^0.1.1"
+ mimic-fn "^1.0.0"
+ p-is-promise "^2.0.0"
+
+memorystream@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
+ integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+ integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
+
+merge-graphql-schemas@^1.5.8:
+ version "1.5.8"
+ resolved "https://registry.yarnpkg.com/merge-graphql-schemas/-/merge-graphql-schemas-1.5.8.tgz#89457b60312aabead44d5b2b7625643f8ab9e369"
+ integrity sha512-0TGOKebltvmWR9h9dPYS2vAqMPThXwJ6gVz7O5MtpBp2sunAg/M25iMSNI7YhU6PDJVtGtldTfqV9a+55YhB+A==
+ dependencies:
+ deepmerge "^2.2.1"
+ glob "^7.1.3"
+ is-glob "^4.0.0"
+
+merge-stream@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1"
+ integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=
+ dependencies:
+ readable-stream "^2.0.1"
+
+methods@^1.1.1, methods@^1.1.2, methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+ integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+
+micromatch@^3.1.10, micromatch@^3.1.4:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
+mime-db@1.40.0:
+ version "1.40.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
+ integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==
+
+mime-db@~1.37.0:
+ version "1.37.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
+ integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==
+
+mime-types@^2.1.12, mime-types@~2.1.19:
+ version "2.1.21"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
+ integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==
+ dependencies:
+ mime-db "~1.37.0"
+
+mime-types@~2.1.24:
+ version "2.1.24"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
+ integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==
+ dependencies:
+ mime-db "1.40.0"
+
+mime@1.6.0, mime@^1.4.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+ integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+mimic-fn@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
+ integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+
+minimalistic-assert@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+ integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+ integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
+
+minimist@^1.1.1, minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+ integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
+
+minimist@~0.0.1:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+ integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
+
+minipass@^2.2.1, minipass@^2.3.4:
+ version "2.3.5"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
+ integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
+minizlib@^1.1.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614"
+ integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==
+ dependencies:
+ minipass "^2.2.1"
+
+mixin-deep@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
+ integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+mkdirp@^0.5.0, mkdirp@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
+ dependencies:
+ minimist "0.0.8"
+
+moment@^2.17.1:
+ version "2.24.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
+ integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
+
+moment@^2.22.2:
+ version "2.22.2"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
+ integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@2.1.1, ms@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+ integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
+mute-stream@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+ integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
+
+mz@^2.4.0:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
+ integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
+ dependencies:
+ any-promise "^1.0.0"
+ object-assign "^4.0.1"
+ thenify-all "^1.0.0"
+
+n3@^0.9.1:
+ version "0.9.1"
+ resolved "https://registry.yarnpkg.com/n3/-/n3-0.9.1.tgz#430b547d58dc7381408c45784dd8058171903932"
+ integrity sha1-QwtUfVjcc4FAjEV4TdgFgXGQOTI=
+
+nan@^2.9.2:
+ version "2.11.1"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
+ integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==
+
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+ integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+
+needle@^2.2.1:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e"
+ integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==
+ dependencies:
+ debug "^2.1.2"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
+negotiator@0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
+ integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
+
+neo4j-driver@^1.7.3, neo4j-driver@~1.7.4:
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.4.tgz#9661cf643b63818bff85e82c4691918e75098c1e"
+ integrity sha512-pbK1HbXh92zNSwMlXL8aNynkHohg9Jx/Tk+EewdJawGm8n8sKIY4NpRkp0nRw6RHvVBU3u9cQXt01ftFVe7j+A==
+ dependencies:
+ babel-runtime "^6.26.0"
+ text-encoding "^0.6.4"
+ uri-js "^4.2.1"
+
+neo4j-graphql-js@^2.6.3:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.6.3.tgz#8f28c2479adda08c90abcc32a784587ef49b8b95"
+ integrity sha512-WZdEqQ8EL9GOIB1ZccbLk1BZz5Dqdbk9i8BDXqxhp1SOI07P9y2cZ244f2Uz4zyES9AVXGmv+861N5xLhrSL2A==
+ dependencies:
+ graphql "^14.2.1"
+ graphql-auth-directives "^2.1.0"
+ lodash "^4.17.11"
+ neo4j-driver "^1.7.3"
+
+next-tick@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+ integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
+
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+ integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+no-case@^2.2.0:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
+ integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==
+ dependencies:
+ lower-case "^1.1.1"
+
+nocache@2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f"
+ integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==
+
+node-environment-flags@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.5.tgz#fa930275f5bf5dae188d6192b24b4c8bbac3d76a"
+ integrity sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==
+ dependencies:
+ object.getownpropertydescriptors "^2.0.3"
+ semver "^5.7.0"
+
+node-fetch@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
+ integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=
+
+node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@~2.6.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
+ integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
+
+node-forge@~0.6.45:
+ version "0.6.49"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.49.tgz#f1ee95d5d74623938fe19d698aa5a26d54d2f60f"
+ integrity sha1-8e6V1ddGI5OP4Z1piqWibVTS9g8=
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+ integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
+
+node-modules-regexp@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
+ integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
+
+node-notifier@^5.2.1:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.3.0.tgz#c77a4a7b84038733d5fb351aafd8a268bfe19a01"
+ integrity sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q==
+ dependencies:
+ growly "^1.3.0"
+ semver "^5.5.0"
+ shellwords "^0.1.1"
+ which "^1.3.0"
+
+node-pre-gyp@^0.10.0:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
+ integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==
+ dependencies:
+ detect-libc "^1.0.2"
+ mkdirp "^0.5.1"
+ needle "^2.2.1"
+ nopt "^4.0.1"
+ npm-packlist "^1.1.6"
+ npmlog "^4.0.2"
+ rc "^1.2.7"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^4"
+
+node-releases@^1.1.19:
+ version "1.1.21"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.21.tgz#46c86f9adaceae4d63c75d3c2f2e6eee618e55f3"
+ integrity sha512-TwnURTCjc8a+ElJUjmDqU6+12jhli1Q61xOQmdZ7ECZVBZuQpN/1UnembiIHDM1wCcfLvh5wrWXUF5H6ufX64Q==
+ dependencies:
+ semver "^5.3.0"
+
+nodemailer@^6.2.1:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.2.1.tgz#20d773925eb8f7a06166a0b62c751dc8290429f3"
+ integrity sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g==
+
+nodemon@~1.19.1:
+ version "1.19.1"
+ resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071"
+ integrity sha512-/DXLzd/GhiaDXXbGId5BzxP1GlsqtMGM9zTmkWrgXtSqjKmGSbLicM/oAy4FR0YWm14jCHRwnR31AHS2dYFHrg==
+ dependencies:
+ chokidar "^2.1.5"
+ debug "^3.1.0"
+ ignore-by-default "^1.0.1"
+ minimatch "^3.0.4"
+ pstree.remy "^1.1.6"
+ semver "^5.5.0"
+ supports-color "^5.2.0"
+ touch "^3.1.0"
+ undefsafe "^2.0.2"
+ update-notifier "^2.5.0"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+nopt@~1.0.10:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
+ integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=
+ dependencies:
+ abbrev "1"
+
+normalize-package-data@^2.3.2:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+normalize-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+npm-bundled@^1.0.1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979"
+ integrity sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==
+
+npm-packlist@^1.1.6:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.12.tgz#22bde2ebc12e72ca482abd67afc51eb49377243a"
+ integrity sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+
+npm-run-all@~4.1.5:
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
+ integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ chalk "^2.4.1"
+ cross-spawn "^6.0.5"
+ memorystream "^0.3.1"
+ minimatch "^3.0.4"
+ pidtree "^0.3.0"
+ read-pkg "^3.0.0"
+ shell-quote "^1.6.1"
+ string.prototype.padend "^3.0.0"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+ dependencies:
+ path-key "^2.0.0"
+
+npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+nth-check@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+ integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+ dependencies:
+ boolbase "~1.0.0"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+ integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+
+numeral@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/numeral/-/numeral-2.0.6.tgz#4ad080936d443c2561aed9f2197efffe25f4e506"
+ integrity sha1-StCAk21EPCVhrtnyGX7//iX05QY=
+
+nwsapi@^2.0.7:
+ version "2.0.9"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.9.tgz#77ac0cdfdcad52b6a1151a84e73254edc33ed016"
+ integrity sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ==
+
+oauth-sign@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+ integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
+object-hash@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
+ integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
+
+object-keys@^1.0.12:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2"
+ integrity sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==
+
+object-path@^0.11.4:
+ version "0.11.4"
+ resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949"
+ integrity sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=
+
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+ dependencies:
+ isobject "^3.0.0"
+
+object.getownpropertydescriptors@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
+ integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.1"
+
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+ dependencies:
+ isobject "^3.0.1"
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+ dependencies:
+ ee-first "1.1.1"
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+ dependencies:
+ wrappy "1"
+
+onetime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+ integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
+ dependencies:
+ mimic-fn "^1.0.0"
+
+optimism@^0.9.0:
+ version "0.9.5"
+ resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.9.5.tgz#b8b5dc9150e97b79ddbf2d2c6c0e44de4d255527"
+ integrity sha512-lNvmuBgONAGrUbj/xpH69FjMOz1d0jvMNoOCKyVynUPzq2jgVlGL4jFYJqrUHzUfBv+jAFSCP61x5UkfbduYJA==
+ dependencies:
+ "@wry/context" "^0.4.0"
+
+optimist@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+ integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY=
+ dependencies:
+ minimist "~0.0.1"
+ wordwrap "~0.0.2"
+
+optionator@^0.8.1, optionator@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+ integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+
+os-locale@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
+ integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
+ dependencies:
+ execa "^1.0.0"
+ lcid "^2.0.0"
+ mem "^4.0.0"
+
+os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+ integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+
+osenv@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
+ integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+output-file-sync@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-2.0.1.tgz#f53118282f5f553c2799541792b723a4c71430c0"
+ integrity sha512-mDho4qm7WgIXIGf4eYU1RHN2UU5tPfVYVSRwDJw0uTmj35DQUt/eNp19N7v6T3SrR0ESTEf2up2CGO73qI35zQ==
+ dependencies:
+ graceful-fs "^4.1.11"
+ is-plain-obj "^1.1.0"
+ mkdirp "^0.5.1"
+
+p-defer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
+ integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
+
+p-each-series@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
+ integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=
+ dependencies:
+ p-reduce "^1.0.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+
+p-is-promise@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.0.0.tgz#7554e3d572109a87e1f3f53f6a7d85d1b194f4c5"
+ integrity sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==
+
+p-limit@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
+ integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
+ dependencies:
+ p-try "^1.0.0"
+
+p-limit@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec"
+ integrity sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
+ dependencies:
+ p-limit "^1.1.0"
+
+p-locate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+ integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+ dependencies:
+ p-limit "^2.0.0"
+
+p-reduce@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
+ integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=
+
+p-try@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+ integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+
+p-try@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
+ integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==
+
+package-json@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed"
+ integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=
+ dependencies:
+ got "^6.7.1"
+ registry-auth-token "^3.0.1"
+ registry-url "^3.0.3"
+ semver "^5.1.0"
+
+pad-right@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/pad-right/-/pad-right-0.2.2.tgz#6fbc924045d244f2a2a244503060d3bfc6009774"
+ integrity sha1-b7ySQEXSRPKiokRQMGDTv8YAl3Q=
+ dependencies:
+ repeat-string "^1.5.2"
+
+parent-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.0.tgz#df250bdc5391f4a085fb589dad761f5ad6b865b5"
+ integrity sha512-8Mf5juOMmiE4FcmzYc4IaiS9L3+9paz2KOiXzkRviCP6aDmN49Hz6EMWz0lGNp9pX80GvvAuLADtyGfW/Em3TA==
+ dependencies:
+ callsites "^3.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
+ dependencies:
+ error-ex "^1.2.0"
+
+parse-json@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+ integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+ dependencies:
+ error-ex "^1.3.1"
+ json-parse-better-errors "^1.0.1"
+
+parse-passwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
+ integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
+
+parse5@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
+ integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
+
+parse5@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+ integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
+ dependencies:
+ "@types/node" "*"
+
+parseurl@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+ integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+ integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-dirname@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+ integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+ integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-is-inside@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+ integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
+
+path-key@^2.0.0, path-key@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+ integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+path-parse@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+ integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-to-regexp@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+ integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+ integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=
+ dependencies:
+ pify "^2.0.0"
+
+path-type@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+ integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
+ dependencies:
+ pify "^3.0.0"
+
+pathval@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
+ integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA=
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+ integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+pidtree@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.0.tgz#f6fada10fccc9f99bf50e90d0b23d72c9ebc2e6b"
+ integrity sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==
+
+pify@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+ integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+ integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
+pirates@^4.0.0, pirates@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
+ integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==
+ dependencies:
+ node-modules-regexp "^1.0.0"
+
+pkg-dir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
+ integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
+ dependencies:
+ find-up "^2.1.0"
+
+pkg-dir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
+ integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
+ dependencies:
+ find-up "^3.0.0"
+
+pkginfo@~0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
+ integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=
+
+pkginfo@~0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
+ integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=
+
+platform@1.3.5:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444"
+ integrity sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==
+
+pn@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+ integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+ integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
+postcss@^7.0.5:
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.6.tgz#6dcaa1e999cdd4a255dcd7d4d9547f4ca010cdc2"
+ integrity sha512-Nq/rNjnHFcKgCDDZYO0lNsl6YWe6U7tTy+ESN+PnLxebL8uBtYX59HZqvrj7YLK5UCyll2hqDsJOo3ndzEW8Ug==
+ dependencies:
+ chalk "^2.4.1"
+ source-map "^0.6.1"
+ supports-color "^5.5.0"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+ integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
+prepend-http@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
+ integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
+
+prettier-linter-helpers@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
+ integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
+ dependencies:
+ fast-diff "^1.1.2"
+
+prettier@~1.18.2:
+ version "1.18.2"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea"
+ integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==
+
+pretty-format@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2"
+ integrity sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw==
+ dependencies:
+ "@jest/types" "^24.8.0"
+ ansi-regex "^4.0.0"
+ ansi-styles "^3.2.0"
+ react-is "^16.8.4"
+
+private@^0.1.6:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+ integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
+
+process-nextick-args@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
+ integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
+
+progress@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
+ integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+
+prompts@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.0.1.tgz#201b3718b4276fb407f037db48c0029d6465245c"
+ integrity sha512-8lnEOSIGQbgbnO47+13S+H204L8ISogGulyi0/NNEFAQ9D1VMNTrJ9SBX2Ra03V4iPn/zt36HQMndRYkaPoWiQ==
+ dependencies:
+ kleur "^3.0.0"
+ sisteransi "^1.0.0"
+
+prop-types@^15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
+ integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==
+ dependencies:
+ loose-envify "^1.3.1"
+ object-assign "^4.1.1"
+
+property-expr@^1.5.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-1.5.1.tgz#22e8706894a0c8e28d58735804f6ba3a3673314f"
+ integrity sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g==
+
+protobufjs@^6.8.6:
+ version "6.8.8"
+ resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c"
+ integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==
+ dependencies:
+ "@protobufjs/aspromise" "^1.1.2"
+ "@protobufjs/base64" "^1.1.2"
+ "@protobufjs/codegen" "^2.0.4"
+ "@protobufjs/eventemitter" "^1.1.0"
+ "@protobufjs/fetch" "^1.1.0"
+ "@protobufjs/float" "^1.0.2"
+ "@protobufjs/inquire" "^1.1.0"
+ "@protobufjs/path" "^1.1.2"
+ "@protobufjs/pool" "^1.1.0"
+ "@protobufjs/utf8" "^1.1.0"
+ "@types/long" "^4.0.0"
+ "@types/node" "^10.1.0"
+ long "^4.0.0"
+
+proxy-addr@~2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34"
+ integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==
+ dependencies:
+ forwarded "~0.1.2"
+ ipaddr.js "1.9.0"
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+ integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
+
+psl@^1.1.24:
+ version "1.1.29"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
+ integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==
+
+psl@^1.1.28:
+ version "1.1.31"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
+ integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
+
+pstree.remy@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.6.tgz#73a55aad9e2d95814927131fbf4dc1b62d259f47"
+ integrity sha512-NdF35+QsqD7EgNEI5mkI/X+UwaxVEbQaz9f4IooEmMUv6ZPmlTQYGjBPJGgrlzNdjSvIy4MWMg6Q6vCgBO2K+w==
+
+pump@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+ integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+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.x.x, punycode@^2.1.0, punycode@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+ integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
+qs@6.7.0, qs@^6.5.1:
+ version "6.7.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
+ integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
+
+qs@~6.5.2:
+ version "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=
+
+range-parser@~1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+ integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+raw-body@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
+ integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
+ dependencies:
+ bytes "3.1.0"
+ http-errors "1.7.2"
+ iconv-lite "0.4.24"
+ unpipe "1.0.0"
+
+rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+ integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+ dependencies:
+ deep-extend "^0.6.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+react-dom@^16.4.2:
+ version "16.6.3"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.3.tgz#8fa7ba6883c85211b8da2d0efeffc9d3825cccc0"
+ integrity sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ scheduler "^0.11.2"
+
+react-is@^16.8.4:
+ version "16.8.4"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2"
+ integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA==
+
+react@^16.4.2:
+ version "16.6.3"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c"
+ integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ scheduler "^0.11.2"
+
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+ integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=
+ dependencies:
+ find-up "^2.0.0"
+ read-pkg "^2.0.0"
+
+read-pkg-up@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978"
+ integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==
+ dependencies:
+ find-up "^3.0.0"
+ read-pkg "^3.0.0"
+
+read-pkg@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+ integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=
+ dependencies:
+ load-json-file "^2.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^2.0.0"
+
+read-pkg@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
+ integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
+ dependencies:
+ load-json-file "^4.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^3.0.0"
+
+readable-stream@1.1.x:
+ version "1.1.14"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
+ integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
+readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.3, readable-stream@^2.3.5:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
+ integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+readable-stream@^3.0.6:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.0.6.tgz#351302e4c68b5abd6a2ed55376a7f9a25be3057a"
+ integrity sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readdirp@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
+ integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==
+ dependencies:
+ graceful-fs "^4.1.11"
+ micromatch "^3.1.10"
+ readable-stream "^2.0.2"
+
+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"
+
+reasoner@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/reasoner/-/reasoner-2.0.0.tgz#6ccf76cb9baf96b82c45ab0bd60211c2aa1b701b"
+ integrity sha1-bM92y5uvlrgsRasL1gIRwqobcBs=
+ dependencies:
+ n3 "^0.9.1"
+ rfc5646 "^2.0.0"
+ vocabs-asx "^0.11.1"
+ vocabs-rdf "^0.11.1"
+ vocabs-rdfs "^0.11.1"
+ vocabs-xsd "^0.11.1"
+
+referrer-policy@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.2.0.tgz#b99cfb8b57090dc454895ef897a4cc35ef67a98e"
+ integrity sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA==
+
+regenerate-unicode-properties@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.2.tgz#7b38faa296252376d363558cfbda90c9ce709662"
+ integrity sha512-SbA/iNrBUf6Pv2zU8Ekv1Qbhv92yxL4hiDa2siuxs4KKn4oOoMDHXjAf7+Nz9qinUQ46B1LcWEi/PhJfPWpZWQ==
+ 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"
+ integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
+
+regenerator-runtime@^0.11.0:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+ integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
+
+regenerator-runtime@^0.12.0:
+ version "0.12.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
+ integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
+
+regenerator-runtime@^0.13.2:
+ version "0.13.2"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
+ integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
+
+regenerator-transform@^0.14.0:
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.0.tgz#2ca9aaf7a2c239dd32e4761218425b8c7a86ecaf"
+ integrity sha512-rtOelq4Cawlbmq9xuMR5gdFmv7ku/sFoB7sRiywx7aq53bc52b4j6zvH7Te1Vt/X2YveDKnCGUbioieU7FEL3w==
+ dependencies:
+ private "^0.1.6"
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
+regexp-tree@^0.1.6:
+ version "0.1.10"
+ resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.10.tgz#d837816a039c7af8a8d64d7a7c3cf6a1d93450bc"
+ integrity sha512-K1qVSbcedffwuIslMwpe6vGlj+ZXRnGkvjAtFHfDZZZuEdA/h0dxljAPu9vhUo6Rrx2U2AwJ+nSQ6hK+lrP5MQ==
+
+regexpp@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
+ integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
+
+regexpu-core@^4.5.4:
+ version "4.5.4"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae"
+ integrity sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ==
+ dependencies:
+ regenerate "^1.4.0"
+ regenerate-unicode-properties "^8.0.2"
+ regjsgen "^0.5.0"
+ regjsparser "^0.6.0"
+ unicode-match-property-ecmascript "^1.0.4"
+ unicode-match-property-value-ecmascript "^1.1.0"
+
+registry-auth-token@^3.0.1:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20"
+ integrity sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==
+ dependencies:
+ rc "^1.1.6"
+ safe-buffer "^5.0.1"
+
+registry-url@^3.0.3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
+ integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI=
+ dependencies:
+ rc "^1.0.1"
+
+regjsgen@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd"
+ integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==
+
+regjsparser@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c"
+ integrity sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==
+ 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"
+ integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
+repeat-element@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+ integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
+
+repeat-string@^1.5.2, repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+ integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+request-promise-core@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6"
+ integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=
+ dependencies:
+ lodash "^4.13.1"
+
+request-promise-native@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5"
+ integrity sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=
+ dependencies:
+ request-promise-core "1.1.1"
+ stealthy-require "^1.1.0"
+ tough-cookie ">=2.3.3"
+
+request@^2.61.0, request@^2.87.0, request@^2.88.0, request@~2.88.0:
+ version "2.88.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+ integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.0"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.4.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+ integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
+
+resolve-cwd@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
+ integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=
+ dependencies:
+ resolve-from "^3.0.0"
+
+resolve-from@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+ integrity sha1-six699nWiBvItuZTM17rywoYh0g=
+
+resolve-from@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+ integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+ integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
+resolve@1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+ integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
+
+resolve@^1.10.1, resolve@^1.11.0, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232"
+ integrity sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==
+ dependencies:
+ path-parse "^1.0.6"
+
+restore-cursor@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+ integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
+ dependencies:
+ onetime "^2.0.0"
+ signal-exit "^3.0.2"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+ integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
+retry@0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
+ integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
+
+rfc5646@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/rfc5646/-/rfc5646-2.0.0.tgz#ac0c67b6cd04411ef7c80751ba159d9371ce116c"
+ integrity sha1-rAxnts0EQR73yAdRuhWdk3HOEWw=
+
+rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
+ integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
+ dependencies:
+ glob "^7.1.3"
+
+rsvp@^3.3.3:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
+ integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==
+
+run-async@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
+ integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA=
+ dependencies:
+ is-promise "^2.1.0"
+
+rx@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
+ integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=
+
+rxjs@^6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504"
+ integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==
+ dependencies:
+ tslib "^1.9.0"
+
+safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+ dependencies:
+ ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sane@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-4.0.3.tgz#e878c3f19e25cc57fbb734602f48f8a97818b181"
+ integrity sha512-hSLkC+cPHiBQs7LSyXkotC3UUtyn8C4FMn50TNaacRyvBlI+3ebcxMpqckmTdtXVtel87YS7GXN3UIOj7NiGVQ==
+ dependencies:
+ "@cnakazawa/watch" "^1.0.3"
+ anymatch "^2.0.0"
+ capture-exit "^1.2.0"
+ exec-sh "^0.3.2"
+ execa "^1.0.0"
+ fb-watchman "^2.0.0"
+ micromatch "^3.1.4"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+
+sanitize-html@~1.20.1:
+ version "1.20.1"
+ resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.1.tgz#f6effdf55dd398807171215a62bfc21811bacf85"
+ integrity sha512-txnH8TQjaQvg2Q0HY06G6CDJLVYCpbnxrdO0WN8gjCKaU5J0KbyGYhZxx5QJg3WLZ1lB7XU9kDkfrCXUozqptA==
+ dependencies:
+ chalk "^2.4.1"
+ htmlparser2 "^3.10.0"
+ lodash.clonedeep "^4.5.0"
+ lodash.escaperegexp "^4.1.2"
+ lodash.isplainobject "^4.0.6"
+ lodash.isstring "^4.0.1"
+ lodash.mergewith "^4.6.1"
+ postcss "^7.0.5"
+ srcset "^1.0.0"
+ 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"
+ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+scheduler@^0.11.2:
+ version "0.11.3"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.3.tgz#b5769b90cf8b1464f3f3cfcafe8e3cd7555a2d6b"
+ integrity sha512-i9X9VRRVZDd3xZw10NY5Z2cVMbdYg6gqFecfj79USv1CFN+YrJ3gIPRKf1qlY+Sxly4djoKdfx1T+m9dnRB8kQ==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
+seed-random@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54"
+ integrity sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=
+
+semver-compare@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
+ integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
+
+semver-diff@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
+ integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=
+ dependencies:
+ semver "^5.0.3"
+
+"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0:
+ version "5.7.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
+ integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
+
+semver@^6.0.0, semver@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.0.tgz#e95dc415d45ecf03f2f9f83b264a6b11f49c0cca"
+ integrity sha512-kCqEOOHoBcFs/2Ccuk4Xarm/KiWRSLEX9CAZF8xkJ6ZPlIoTZ8V5f7J16vYLJqDbR7KrxTJpR2lqjIEm2Qx9cQ==
+
+send@0.17.1:
+ version "0.17.1"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
+ integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
+ dependencies:
+ debug "2.6.9"
+ depd "~1.1.2"
+ destroy "~1.0.4"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "0.5.2"
+ http-errors "~1.7.2"
+ mime "1.6.0"
+ ms "2.1.1"
+ on-finished "~2.3.0"
+ range-parser "~1.2.1"
+ statuses "~1.5.0"
+
+serialize-error@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-3.0.0.tgz#80100282b09be33c611536f50033481cb9cc87cf"
+ integrity sha512-+y3nkkG/go1Vdw+2f/+XUXM1DXX1XcxTl99FfiD/OEPUNw4uo0i6FKABfTAN5ZcgGtjTRZcEbxcE/jtXbEY19A==
+
+serve-static@1.14.1:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
+ integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
+ dependencies:
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ parseurl "~1.3.3"
+ send "0.17.1"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+set-value@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+ integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.1"
+ to-object-path "^0.3.0"
+
+set-value@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
+ integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
+setprototypeof@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+ integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
+
+setprototypeof@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
+ integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
+
+sha.js@^2.4.11:
+ version "2.4.11"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+ integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+ integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+shell-quote@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
+ integrity sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=
+ dependencies:
+ array-filter "~0.0.0"
+ array-map "~0.0.0"
+ array-reduce "~0.0.0"
+ jsonify "~0.0.0"
+
+shellwords@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+ integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+ integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
+
+sisteransi@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.0.tgz#77d9622ff909080f1c19e5f4a1df0c1b0a27b88c"
+ integrity sha512-N+z4pHB4AmUv0SjveWRd6q1Nj5w62m5jodv+GD8lvmbY/83T/rpbJGZOnK5T149OldDj4Db07BSv9xY4K6NTPQ==
+
+slash@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+ integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+
+slice-ansi@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
+ integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
+ dependencies:
+ ansi-styles "^3.2.0"
+ astral-regex "^1.0.0"
+ is-fullwidth-code-point "^2.0.0"
+
+slug@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/slug/-/slug-1.1.0.tgz#73eef5710416f515077bdf70c683bde4915913c9"
+ integrity sha512-NuIOjDQeTMPm+/AUIHJ5636mF3jOsYLFnoEErl9Tdpt4kpt4fOrAJxscH9mUgX1LtPaEqgPCawBg7A4yhoSWRg==
+ dependencies:
+ unicode ">= 0.3.1"
+
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+ integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^3.1.0"
+
+source-map-resolve@^0.5.0:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
+ integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==
+ dependencies:
+ atob "^2.1.1"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
+source-map-support@^0.5.1, source-map-support@^0.5.6, source-map-support@^0.5.9:
+ version "0.5.9"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"
+ integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+ integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+
+source-map@0.5.6:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
+ integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
+
+source-map@^0.5.0, source-map@^0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+ integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spdx-correct@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
+ integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==
+ dependencies:
+ spdx-expression-parse "^3.0.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
+ integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==
+
+spdx-expression-parse@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
+ integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==
+ dependencies:
+ spdx-exceptions "^2.1.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz#a59efc09784c2a5bada13cfeaf5c75dd214044d2"
+ integrity sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg==
+
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+ dependencies:
+ extend-shallow "^3.0.0"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+ integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+srcset@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef"
+ integrity sha1-pWad4StC87HV6D7QPHEEb8SPQe8=
+ dependencies:
+ array-uniq "^1.0.2"
+ number-is-nan "^1.0.0"
+
+sshpk@^1.7.0:
+ version "1.15.2"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.2.tgz#c946d6bd9b1a39d0e8635763f5242d6ed6dcb629"
+ integrity sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ bcrypt-pbkdf "^1.0.0"
+ dashdash "^1.12.0"
+ ecc-jsbn "~0.1.1"
+ getpass "^0.1.1"
+ jsbn "~0.1.0"
+ safer-buffer "^2.0.2"
+ tweetnacl "~0.14.0"
+
+stack-chain@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-2.0.0.tgz#d73d1172af89565f07438b5bcc086831b6689b2d"
+ integrity sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==
+
+stack-generator@^2.0.1:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.3.tgz#bb74385c67ffc4ccf3c4dee5831832d4e509c8a0"
+ integrity sha512-kdzGoqrnqsMxOEuXsXyQTmvWXZmG0f3Ql2GDx5NtmZs59sT2Bt9Vdyq0XdtxUi58q/+nxtbF9KOQ9HkV1QznGg==
+ dependencies:
+ stackframe "^1.0.4"
+
+stack-utils@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
+ integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
+
+stackframe@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"
+ integrity sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw==
+
+stacktrace-gps@^3.0.1:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.2.tgz#33f8baa4467323ab2bd1816efa279942ba431ccc"
+ integrity sha512-9o+nWhiz5wFnrB3hBHs2PTyYrS60M1vvpSzHxwxnIbtY2q9Nt51hZvhrG1+2AxD374ecwyS+IUwfkHRE/2zuGg==
+ dependencies:
+ source-map "0.5.6"
+ stackframe "^1.0.4"
+
+stacktrace-js@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.0.tgz#776ca646a95bc6c6b2b90776536a7fc72c6ddb58"
+ integrity sha1-d2ymRqlbxsayuQd2U2p/xyxt21g=
+ dependencies:
+ error-stack-parser "^2.0.1"
+ stack-generator "^2.0.1"
+ stacktrace-gps "^3.0.1"
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
+"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+ integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+
+stealthy-require@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+ integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
+streamsearch@0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
+ integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
+
+string-argv@0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.1.tgz#66bd5ae3823708eaa1916fa5412703150d4ddfaf"
+ integrity sha512-El1Va5ehZ0XTj3Ekw4WFidXvTmt9SrC0+eigdojgtJMVtPkF0qbBe9fyNSl9eQf+kUHnTSQxdQYzuHfZy8V+DQ==
+
+string-length@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
+ integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=
+ dependencies:
+ astral-regex "^1.0.0"
+ strip-ansi "^4.0.0"
+
+string-width@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string-width@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.0.0.tgz#5a1690a57cc78211fffd9bf24bbe24d090604eb1"
+ integrity sha512-rr8CUxBbvOZDUvc5lNIJ+OC1nPVpz+Siw9VBtUjB9b6jZehZLFt0JMCZzShFHIsI8cbhm0EsNIfWJMFV3cu3Ew==
+ dependencies:
+ emoji-regex "^7.0.1"
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^5.0.0"
+
+string.prototype.padend@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0"
+ integrity sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.4.3"
+ function-bind "^1.0.2"
+
+string_decoder@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
+ integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+ integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
+
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f"
+ integrity sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==
+ dependencies:
+ ansi-regex "^4.0.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+ integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+ integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+
+strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+ integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
+
+subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.8:
+ version "0.9.15"
+ resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.15.tgz#68a8b7ba0037d8c489fb2f5a102d1494db297d0d"
+ integrity sha512-f9eBfWdHsePQV67QIX+VRhf++dn1adyC/PZHP6XI5AfKnZ4n0FW+v5omxwdHVpd4xq2ZijaHEcmlQrhBY79ZWQ==
+ dependencies:
+ backo2 "^1.0.2"
+ eventemitter3 "^3.1.0"
+ iterall "^1.2.1"
+ symbol-observable "^1.0.4"
+ ws "^5.2.0"
+
+superagent@^3.8.3:
+ version "3.8.3"
+ resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128"
+ integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==
+ dependencies:
+ component-emitter "^1.2.0"
+ cookiejar "^2.1.0"
+ debug "^3.1.0"
+ extend "^3.0.0"
+ form-data "^2.3.1"
+ formidable "^1.2.0"
+ methods "^1.1.1"
+ mime "^1.4.1"
+ qs "^6.5.1"
+ readable-stream "^2.3.5"
+
+supertest@~4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/supertest/-/supertest-4.0.2.tgz#c2234dbdd6dc79b6f15b99c8d6577b90e4ce3f36"
+ integrity sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==
+ dependencies:
+ methods "^1.1.2"
+ superagent "^3.8.3"
+
+supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.5.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^6.0.0, supports-color@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+ integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+ dependencies:
+ has-flag "^3.0.0"
+
+symbol-observable@^1.0.2, symbol-observable@^1.0.4:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
+ integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
+
+symbol-tree@^3.2.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
+ integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=
+
+synchronous-promise@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.6.tgz#de76e0ea2b3558c1e673942e47e714a930fa64aa"
+ integrity sha512-TyOuWLwkmtPL49LHCX1caIwHjRzcVd62+GF6h8W/jHOeZUFHpnd2XJDVuUlaTaLPH1nuu2M69mfHr5XbQJnf/g==
+
+table@^5.2.3:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/table/-/table-5.2.3.tgz#cde0cc6eb06751c009efab27e8c820ca5b67b7f2"
+ integrity sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ==
+ dependencies:
+ ajv "^6.9.1"
+ lodash "^4.17.11"
+ slice-ansi "^2.1.0"
+ string-width "^3.0.0"
+
+tar@^4:
+ version "4.4.8"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d"
+ integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==
+ dependencies:
+ chownr "^1.1.1"
+ fs-minipass "^1.2.5"
+ minipass "^2.3.4"
+ minizlib "^1.1.1"
+ mkdirp "^0.5.0"
+ safe-buffer "^5.1.2"
+ yallist "^3.0.2"
+
+term-size@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
+ integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=
+ dependencies:
+ execa "^0.7.0"
+
+test-exclude@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.1.0.tgz#6ba6b25179d2d38724824661323b73e03c0c1de1"
+ integrity sha512-gwf0S2fFsANC55fSeSqpb8BYk6w3FDvwZxfNjeF6FRgvFa43r+7wRiA/Q0IxoRU37wB/LE8IQ4221BsNucTaCA==
+ dependencies:
+ arrify "^1.0.1"
+ minimatch "^3.0.4"
+ read-pkg-up "^4.0.0"
+ require-main-filename "^1.0.1"
+
+text-encoding@^0.6.4:
+ version "0.6.4"
+ resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
+ integrity sha1-45mpgiV6J22uQou5KEXLcb3CbRk=
+
+text-table@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+ integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
+
+thenify-all@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
+ integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=
+ dependencies:
+ thenify ">= 3.1.0 < 4"
+
+"thenify@>= 3.1.0 < 4":
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839"
+ integrity sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=
+ dependencies:
+ any-promise "^1.0.0"
+
+throat@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
+ integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
+
+through@^2.3.6:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+ integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+
+timed-out@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
+ integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=
+
+title-case@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa"
+ integrity sha1-PhJyFtpY0rxb7PE3q5Ha46fNj6o=
+ dependencies:
+ no-case "^2.2.0"
+ upper-case "^1.0.3"
+
+tmp@^0.0.33:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
+ dependencies:
+ os-tmpdir "~1.0.2"
+
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+ integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+ integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
+toidentifier@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
+ integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+
+topo@3.x.x:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.3.tgz#d5a67fb2e69307ebeeb08402ec2a2a6f5f7ad95c"
+ integrity sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==
+ dependencies:
+ hoek "6.x.x"
+
+toposort@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
+ integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
+
+touch@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
+ integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==
+ dependencies:
+ nopt "~1.0.10"
+
+tough-cookie@>=2.3.3, tough-cookie@^2.3.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+ integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+ dependencies:
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
+tough-cookie@~2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
+ integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
+ dependencies:
+ psl "^1.1.24"
+ punycode "^1.4.1"
+
+tr46@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+ integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
+ dependencies:
+ punycode "^2.1.0"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+ integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
+
+trunc-html@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/trunc-html/-/trunc-html-1.1.2.tgz#1e97d51f67d470b67662b1a670e6d0ea7a8edafe"
+ integrity sha1-HpfVH2fUcLZ2YrGmcObQ6nqO2v4=
+ dependencies:
+ assignment "2.2.0"
+ insane "2.6.1"
+ trunc-text "1.0.1"
+
+trunc-text@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trunc-text/-/trunc-text-1.0.1.tgz#58f876d8ac59b224b79834bb478b8656e69622b5"
+ integrity sha1-WPh22KxZsiS3mDS7R4uGVuaWIrU=
+
+ts-invariant@^0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.2.tgz#8685131b8083e67c66d602540e78763408be9113"
+ integrity sha512-PTAAn8lJPEdRBJJEs4ig6MVZWfO12yrFzV7YaPslmyhG7+4MA279y4BXT3f72gXeVl0mC1aAWq2rMX4eKTWU/Q==
+ dependencies:
+ tslib "^1.9.3"
+
+tslib@^1.9.0, tslib@^1.9.3:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
+ integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+ integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+ dependencies:
+ prelude-ls "~1.1.2"
+
+type-detect@^4.0.0, type-detect@^4.0.5:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+ integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
+type-is@^1.6.16, type-is@~1.6.17, type-is@~1.6.18:
+ version "1.6.18"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
+ integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.24"
+
+uglify-js@^3.1.4:
+ version "3.4.9"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
+ integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==
+ dependencies:
+ commander "~2.17.1"
+ source-map "~0.6.1"
+
+undefsafe@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76"
+ integrity sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=
+ dependencies:
+ debug "^2.2.0"
+
+unicode-canonical-property-names-ecmascript@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
+ integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
+
+unicode-match-property-ecmascript@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"
+ integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
+ dependencies:
+ unicode-canonical-property-names-ecmascript "^1.0.4"
+ unicode-property-aliases-ecmascript "^1.0.4"
+
+unicode-match-property-value-ecmascript@^1.1.0:
+ version "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-property-aliases-ecmascript@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0"
+ integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==
+
+"unicode@>= 0.3.1":
+ version "11.0.1"
+ resolved "https://registry.yarnpkg.com/unicode/-/unicode-11.0.1.tgz#735bd422ec75cf28d396eb224d535d168d5f1db6"
+ integrity sha512-+cHtykLb+eF1yrSLWTwcYBrqJkTfX7Quoyg7Juhe6uylF43ZbMdxMuSHNYlnyLT8T7POAvavgBthzUF9AIaQvQ==
+
+union-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+ integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^0.4.3"
+
+unique-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
+ integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=
+ dependencies:
+ crypto-random-string "^1.0.0"
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+ integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+unzip-response@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
+ integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=
+
+upath@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068"
+ integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==
+
+update-notifier@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6"
+ integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==
+ dependencies:
+ boxen "^1.2.1"
+ chalk "^2.0.1"
+ configstore "^3.0.0"
+ import-lazy "^2.1.0"
+ is-ci "^1.0.10"
+ is-installed-globally "^0.1.0"
+ is-npm "^1.0.0"
+ latest-version "^3.0.0"
+ semver-diff "^2.0.0"
+ xdg-basedir "^3.0.0"
+
+upper-case@^1.0.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
+ integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=
+
+uri-js@^4.2.1, uri-js@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+ integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+ dependencies:
+ punycode "^2.1.0"
+
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+ integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+url-parse-lax@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73"
+ integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=
+ dependencies:
+ prepend-http "^1.0.1"
+
+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"
+ integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+
+util-arity@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/util-arity/-/util-arity-1.1.0.tgz#59d01af1fdb3fede0ac4e632b0ab5f6ce97c9330"
+ integrity sha1-WdAa8f2z/t4KxOYysKtfbOl8kzA=
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util.promisify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
+ integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
+ dependencies:
+ define-properties "^1.1.2"
+ object.getownpropertydescriptors "^2.0.3"
+
+util@0.10.3:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk=
+ dependencies:
+ inherits "2.0.1"
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+ integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+
+uuid@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
+ integrity sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==
+
+uuid@^3.1.0, uuid@^3.3.2, 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==
+
+v8flags@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.1.1.tgz#42259a1461c08397e37fe1d4f1cfb59cad85a053"
+ integrity sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==
+ dependencies:
+ homedir-polyfill "^1.0.1"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
+ integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
+ dependencies:
+ spdx-correct "^3.0.0"
+ spdx-expression-parse "^3.0.0"
+
+vary@^1, vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+ integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+
+verror@1.10.0, verror@^1.9.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+vocabs-as@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/vocabs-as/-/vocabs-as-3.0.0.tgz#0dd0549cecb331ba4e917d2c5a4e83b146865c23"
+ integrity sha512-Dfze+B0CYZzhSK12jWvbxaL8/vXPnlzhhqhQTrEVxkGht+qzU4MmSLXSomQrdiSNKokVVtt16tyKoJWBW9TdNQ==
+ dependencies:
+ activitystreams-context ">=3.0.0"
+ vocabs ">=0.11.2"
+
+vocabs-asx@^0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/vocabs-asx/-/vocabs-asx-0.11.1.tgz#6667e4e174dc4556722b6cb1b9619fb16491519a"
+ integrity sha1-Zmfk4XTcRVZyK2yxuWGfsWSRUZo=
+ dependencies:
+ vocabs ">=0.11.1"
+
+vocabs-interval@^0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/vocabs-interval/-/vocabs-interval-0.11.1.tgz#1c009421f3e88a307aafbb75bfa670ff0f4f6d3c"
+ integrity sha1-HACUIfPoijB6r7t1v6Zw/w9PbTw=
+ dependencies:
+ vocabs ">=0.11.1"
+
+vocabs-ldp@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/vocabs-ldp/-/vocabs-ldp-0.1.0.tgz#da1728df560471750dfc7050e7e2df1bab901ce6"
+ integrity sha1-2hco31YEcXUN/HBQ5+LfG6uQHOY=
+ dependencies:
+ vocabs ">=0.11.1"
+
+vocabs-owl@^0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/vocabs-owl/-/vocabs-owl-0.11.1.tgz#2355bbd27bfc19c5992d98079bbab3d7d65459e9"
+ integrity sha1-I1W70nv8GcWZLZgHm7qz19ZUWek=
+ dependencies:
+ vocabs ">=0.11.1"
+
+vocabs-rdf@^0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/vocabs-rdf/-/vocabs-rdf-0.11.1.tgz#c7fa91d83b050ffb7b98ce2c72ab25c6fbcd1194"
+ integrity sha1-x/qR2DsFD/t7mM4scqslxvvNEZQ=
+ dependencies:
+ vocabs ">=0.11.1"
+
+vocabs-rdfs@^0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/vocabs-rdfs/-/vocabs-rdfs-0.11.1.tgz#2e2df56ae0de008585b21057570386018da455bf"
+ integrity sha1-Li31auDeAIWFshBXVwOGAY2kVb8=
+ dependencies:
+ vocabs ">=0.11.1"
+
+vocabs-social@^0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/vocabs-social/-/vocabs-social-0.11.1.tgz#d28545868cce325ba0c88e394f3de6e03fad85b1"
+ integrity sha1-0oVFhozOMlugyI45Tz3m4D+thbE=
+ dependencies:
+ vocabs ">=0.11.1"
+
+vocabs-xsd@^0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/vocabs-xsd/-/vocabs-xsd-0.11.1.tgz#20e201d8fd0fd330d6650d9061fda60baae6cd6c"
+ integrity sha1-IOIB2P0P0zDWZQ2QYf2mC6rmzWw=
+ dependencies:
+ vocabs ">=0.11.1"
+
+vocabs@>=0.11.1, vocabs@>=0.11.2:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/vocabs/-/vocabs-0.11.2.tgz#8944b40f11d415f07db6e259804024a1dbfaa4d4"
+ integrity sha512-OIon2MWA21ZO42UBsTa5DuMsk5zv72DxMdQNvLsPN1M9GrjVTovn3LgWUZdPVnKBpdWhqWV7Mfbq/Sh0vkHIBw==
+
+w3c-hr-time@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
+ integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=
+ dependencies:
+ browser-process-hrtime "^0.1.2"
+
+wait-on@~3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-3.2.0.tgz#c83924df0fc42a675c678324c49c769d378bcb85"
+ integrity sha512-QUGNKlKLDyY6W/qHdxaRlXUAgLPe+3mLL/tRByHpRNcHs/c7dZXbu+OnJWGNux6tU1WFh/Z8aEwvbuzSAu79Zg==
+ dependencies:
+ core-js "^2.5.7"
+ joi "^13.0.0"
+ minimist "^1.2.0"
+ request "^2.88.0"
+ rx "^4.1.0"
+
+walker@^1.0.7, walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=
+ dependencies:
+ makeerror "1.0.x"
+
+webidl-conversions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+ integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+ integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+ dependencies:
+ iconv-lite "0.4.24"
+
+whatwg-fetch@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
+ integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==
+
+whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+ integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+
+whatwg-url@^6.4.1:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
+ integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
+whatwg-url@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd"
+ integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+ integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@^1.2.9, which@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
+ integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
+ dependencies:
+ string-width "^1.0.2 || 2"
+
+widest-line@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc"
+ integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==
+ dependencies:
+ string-width "^2.1.1"
+
+wordwrap@~0.0.2:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+ integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+ integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+write-file-atomic@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529"
+ integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ signal-exit "^3.0.2"
+
+write-file-atomic@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
+ integrity sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ signal-exit "^3.0.2"
+
+write@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
+ integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
+ dependencies:
+ mkdirp "^0.5.1"
+
+ws@^5.2.0:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
+ integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
+ dependencies:
+ async-limiter "~1.0.0"
+
+ws@^6.0.0:
+ version "6.1.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8"
+ integrity sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==
+ dependencies:
+ async-limiter "~1.0.0"
+
+x-xss-protection@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.1.0.tgz#4f1898c332deb1e7f2be1280efb3e2c53d69c1a7"
+ integrity sha512-rx3GzJlgEeZ08MIcDsU2vY2B1QEriUKJTSiNHHUIem6eg9pzVOr2TL3Y4Pd6TMAM5D5azGjcxqI62piITBDHVg==
+
+xdg-basedir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
+ integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
+
+xml-name-validator@^3.0.0:
+ version "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.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==
+ dependencies:
+ sax ">=0.6.0"
+ xmlbuilder "~9.0.1"
+
+xmlbuilder@~9.0.1:
+ version "9.0.7"
+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
+ integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
+
+xmldom@0.1.19:
+ version "0.1.19"
+ resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc"
+ integrity sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw=
+
+xtend@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+ integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
+
+"y18n@^3.2.1 || ^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+ integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+ integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
+
+yallist@^3.0.0, yallist@^3.0.2:
+ version "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@^11.1.1:
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
+ integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs@^12.0.2:
+ version "12.0.5"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
+ integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^1.2.0"
+ find-up "^3.0.0"
+ get-caller-file "^1.0.1"
+ os-locale "^3.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1 || ^4.0.0"
+ yargs-parser "^11.1.1"
+
+yup@^0.27.0:
+ version "0.27.0"
+ resolved "https://registry.yarnpkg.com/yup/-/yup-0.27.0.tgz#f8cb198c8e7dd2124beddc2457571329096b06e7"
+ integrity sha512-v1yFnE4+u9za42gG/b/081E7uNW9mUj3qtkmelLbW5YPROZzSH/KUUyJu9Wt8vxFJcT9otL/eZopS0YK1L5yPQ==
+ dependencies:
+ "@babel/runtime" "^7.0.0"
+ fn-name "~2.0.1"
+ lodash "^4.17.11"
+ property-expr "^1.5.0"
+ synchronous-promise "^2.0.6"
+ toposort "^2.0.2"
+
+zen-observable-ts@^0.8.19:
+ version "0.8.19"
+ resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz#c094cd20e83ddb02a11144a6e2a89706946b5694"
+ integrity sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ==
+ dependencies:
+ tslib "^1.9.3"
+ zen-observable "^0.8.0"
+
+zen-observable@^0.8.0:
+ version "0.8.11"
+ resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.11.tgz#d3415885eeeb42ee5abb9821c95bb518fcd6d199"
+ integrity sha512-N3xXQVr4L61rZvGMpWe8XoCGX8vhU35dPyQ4fm5CY/KDlG0F75un14hjbckPXTDuKUY6V0dqR2giT6xN8Y4GEQ==
diff --git a/Human-Connection/cypress.env.template.json b/Human-Connection/cypress.env.template.json
new file mode 100644
index 000000000..bd03f6381
--- /dev/null
+++ b/Human-Connection/cypress.env.template.json
@@ -0,0 +1,6 @@
+{
+ "SEED_SERVER_HOST": "http://localhost:4001",
+ "NEO4J_URI": "bolt://localhost:7687",
+ "NEO4J_USERNAME": "neo4j",
+ "NEO4J_PASSWORD": "letmein"
+}
diff --git a/Human-Connection/cypress.json b/Human-Connection/cypress.json
new file mode 100644
index 000000000..f41489007
--- /dev/null
+++ b/Human-Connection/cypress.json
@@ -0,0 +1,5 @@
+{
+ "projectId": "qa7fe2",
+ "ignoreTestFiles": "*.js",
+ "baseUrl": "http://localhost:3000"
+}
diff --git a/Human-Connection/cypress/README.md b/Human-Connection/cypress/README.md
new file mode 100644
index 000000000..92b1b8185
--- /dev/null
+++ b/Human-Connection/cypress/README.md
@@ -0,0 +1,50 @@
+# End-to-End Testing
+
+## Configure cypress
+
+First, you have to tell cypress how to connect to your local neo4j database
+among other things. You can copy our template configuration and change the new
+file according to your needs.
+
+Make sure you are at the root level of the project. Then:
+```bash
+# in the top level folder Human-Connection/
+$ cp cypress.env.template.json cypress.env.json
+```
+
+## Run Tests
+
+To run the tests, do this:
+
+```bash
+# in the top level folder Human-Connection/
+$ yarn cypress:setup
+```
+
+After verifying that there are no errors with the servers starting, open another tab in your terminal and run the following command:
+
+```bash
+$ yarn cypress:run
+```
+
+
+
+After the test runs, you will also get some video footage of the test run which you can then analyse in more detail.
+
+## Open Interactive Test Console
+
+If you are like me, you might want to see some visual output. The interactive cypress environment also helps at debugging your tests, you can even time travel between individual steps and see the exact state of the app.
+
+To use this feature, you will still run the `yarn cypress:setup` above, but instead of `yarn cypress:run` open another tab in your terminal and run the following command:
+
+```bash
+$ yarn cypress:open
+```
+
+
+
+## Write some Tests
+
+Check out the Cypress documentation for further information on how to write tests:
+[https://docs.cypress.io/guides/getting-started/writing-your-first-test.html\#Write-a-simple-test](https://docs.cypress.io/guides/getting-started/writing-your-first-test.html#Write-a-simple-test)
+
diff --git a/Human-Connection/cypress/features.md b/Human-Connection/cypress/features.md
new file mode 100644
index 000000000..eb8292c3b
--- /dev/null
+++ b/Human-Connection/cypress/features.md
@@ -0,0 +1,279 @@
+# Network Specification
+
+Human Connection is a nonprofit social, action and knowledge network that connects information to action and promotes positive local and global change in all areas of life.
+
+* **Social**: Interact with other people not just by commenting their posts, but by providing **Pro & Contra** arguments, give a **Versus** or ask them by integrated **Chat** or **Let's Talk**
+* **Knowledge**: Read articles about interesting topics and find related posts in the **More Info** tab or by **Filtering** based on **Categories** and **Tagging** or by using the **Fulltext Search**.
+* **Action**: Don't just read about how to make the world a better place, but come into **Action** by following provided suggestions on the **Action** tab provided by other people or **Organisations**.
+
+## Features
+
+The following features will be implemented. This gets done in three steps:
+
+1. First we will implement a basic feature set and provide a test system to test the basic network functionality.
+2. In a second step we will make our prototype publicly available with an advanced feature set including the technology and organizational structure to drive a bigger public social network.
+3. In a third step all the remaining features will be implemented to build the full product.
+
+### User Account
+
+[Cucumber Features](./integration/user_account)
+
+* Sign-up
+* Agree to Data Privacy Statement
+* Agree to Terms of Service
+* Login
+* Logoff
+* Change User Name
+* Change Email Address
+* Change Password
+* Delete Account
+* Download User's Content
+* GDPR-Information about stored Content
+* Choosing Interface Language \(e.g. German / English / French\)
+* Persistent Links
+
+### User Profile
+
+[Cucumber Features](./integration/user_profile)
+
+* Upload and Change Avatar
+* Upload and Change Profile Picture
+* Edit Social Media Accounts
+* Edit Locale information
+* Show and delete Bookmarks \(later\)
+* Show Posts of a specific User
+* Show Comments of a specific User
+
+### Dashboard
+
+[Clickdummy](https://preview.uxpin.com/24a2ab8adcd84f9a763d87ed27251351225e0ecd#/pages/99768919/simulate/sitemap?mode=i)
+
+* Show Link to own Profile
+* Show Friends Widget
+* Show Favorites Widget
+* Show Get Friends Widget
+* Show popular Hashtags Widget
+* Show Mini-Statistics Widget \(all time\)
+* Show Chatrooms Widget
+* Show List of Let's Talk requests with online status of requesting people
+
+### Posts
+
+[Cucumber Features](./integration/post/)
+
+* Creating Posts
+* Persistent Links
+* Upload Teaser Picture for Post
+* Upload additional Pictures
+* Editing Title and Content
+* Allow embedded Conten \(Videos, Sound, ...\)
+* Choosing a Category
+* Adding Tags
+* Choosing Language \(e.g. German / English / French\)
+* Choosing Visibility \(Public / Friends / Private\)
+* Shout Button for Posts
+* Bookmark Posts \(later\)
+* Optionally provide Let's Talk Feature
+* Optionally provide Commenting Feature
+
+### Comments
+
+* Creating Comments
+* Deleting Comments
+* Editing Comments
+* Upvote comments of others
+
+### Notifications
+[Cucumber features](./integration/notifications)
+
+* User @-mentionings
+* Notify authors for comments
+* Administrative notifications to all users
+
+### Contribution List
+
+* Show Posts by Tiles
+* Show Posts as List
+* Filter by Category \(Health and Wellbeing, Global Peace & Non-Violence, ...\)
+* Filter by Mood \(Funny, Happy, Surprised, Cry, Angry, ...\)
+* Filter by Source \(Connections, Following, Individuals, Non-Profits, ...\)
+* Filter by Posts & Tools \(Post, Events, CanDos, ...\)
+* Filter by Format Type \(Text, Pictures, Video, ...\)
+* Extended Filter \(Continent, Country, Language, ...\)
+* Sort Posts by Date
+* Sort Posts by Shouts
+* Sort Posts by most Comments
+* Sort Posts by Emoji-Count \(all Types\)
+
+### Blacklist
+
+[Video](https://www.youtube.com/watch?v=-uDvvmN8hLQ)
+
+* Blacklist Users
+* Blacklist specific Terms
+* Blacklist Tags
+* Switch on/off Adult Content
+
+### Search
+
+[Cucumber Features](./integration/search)
+
+* Search for Categories
+* Search for Tags
+* Fulltext Search
+
+### CanDos
+
+* Creating CanDos
+* Editing Title and Content
+* Choosing a Category
+* Adding Tags
+* Choosing Language \(e.g. German / English / French\)
+* Choosing Visibility \(Public / Friends / Private\)
+* Choosing Difficulty
+* Editing Why - why should you do this
+* Editing Usefulness - what is it good for
+
+### Versus \(interaction on existing Post\)
+
+* Create / edit / delete Versus
+
+### Jobs
+
+* Create, edit and delete Jobs by an User
+* Handle Jobs as Part of Projects
+* Handle Jobs done by Organizations
+
+### Projects
+
+* Create, edit and delete Projects
+* Edit Title and Description for the Project
+* Set Project Type
+* Set and Edit Timeline for the Project
+* Add Media to the Project
+* Chat about the Project
+
+### Pro & Contra
+
+* Create Pro and Con \(2-row\)
+* Add Arguments on Pro or Con Side
+* Rate up Arguments
+* Add Tags
+* Attach Media
+
+### Votes
+
+* Create Votes \(Surveys with two or more Choices\)
+* Add Title and Description
+* Let Users vote
+* Add Tags
+
+### Bestlist
+
+* Create Bestlist
+* Create Votes \(Surveys\)
+* Add Title and Description
+* Add Tags
+* Let Users vote for Best Item
+* Set Settings \(allow Uploads, allow Links, ...\)
+
+### Events
+
+* Create Events
+* Add Title and Description
+* Choose Date and Location
+* Add Tags
+
+### More Info
+
+Shows autmatically releated information for existing post.
+
+* Show related Posts
+* Show Pros and Cons
+* Show Bestlist
+* Show Votes
+* Link to corresponding Chatroom
+
+### Take Action
+
+Shows automatically related actions for existing post.
+
+* Show related Organisations
+* Show related CanDos
+* Show related Projects
+* Show related Jobs
+* Show related Events
+* Show Map
+
+### Badges System
+
+* Importing Badge Information \(CSV\)
+* Showing Badges
+* Badge Administration by Admins
+* Choosing Badges to display by User
+
+### Chat
+
+* Basic 1:1 Chat functionality
+
+### Let's Talk
+
+* Request Let's talk with Author of Post
+* Requestor can request private or public Let's Talk
+* Requestor can choose the Chat format \(Video, Audio, Text\)
+* Interact with interested People 1:1
+* Approve request from Requestor
+
+### Organizations
+
+* Propose Organizations by users
+* Set Name and Details
+* Set Homepage
+* Set Region
+* Set Topic
+* Commit organizations by HC-Org-Team
+* Panel for Organisation Handling by themselfes
+* Choose/Mark Users as authorized to manage an Organization
+
+### Moderation
+
+[Cucumber Features](./integration/moderation)
+
+* Report Button for users for doubtful Content
+* Moderator Panel
+* List of reported Content \(later replaced by User-Moderation\)
+* Mark verified Users as Moderators
+* Show Posts to be moderated highlighted to User-Moderators
+* Statistics about kinds of reported Content by Time
+* Statistics about Decisions in Moderation
+
+### Administration
+
+* Provide Admin-Interface to send Users Invite Code
+* Static Pages for Data Privacy Statement ...
+* Create, edit and delete Announcements
+* Show Announcements on top of User Interface
+
+### Invitation
+
+* Allow Users to invite others by Email
+* Allow Users to register with Invite Code
+* Double-opt-in by Email
+
+### Internationalization
+
+[Cucumber Features](./integration/internationalization)
+
+* Frontend UI
+* Backend Error Messages
+
+### Federation
+
+* Provide Server-Server ActivityPub-API
+* Provide User-Server Activitypub-API
+* Receiving public addressed Article and Note Objects
+* Receiving Like and Follow Activities
+* Receiving Undo and Delete Activities for Articles and Notes
+* Serving Webfinger records and Actor Objects
+* Serving Followers, Following and Outbox collections
+
diff --git a/Human-Connection/cypress/fixtures/example.json b/Human-Connection/cypress/fixtures/example.json
new file mode 100644
index 000000000..da18d9352
--- /dev/null
+++ b/Human-Connection/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
\ No newline at end of file
diff --git a/Human-Connection/cypress/fixtures/onourjourney.png b/Human-Connection/cypress/fixtures/onourjourney.png
new file mode 100644
index 000000000..8e606fabd
Binary files /dev/null and b/Human-Connection/cypress/fixtures/onourjourney.png differ
diff --git a/Human-Connection/cypress/fixtures/users.json b/Human-Connection/cypress/fixtures/users.json
new file mode 100644
index 000000000..339866774
--- /dev/null
+++ b/Human-Connection/cypress/fixtures/users.json
@@ -0,0 +1,17 @@
+{
+ "admin": {
+ "email": "admin@example.org",
+ "password": "1234",
+ "name": "Peter Lustig"
+ },
+ "moderator": {
+ "email": "moderator@example.org",
+ "password": "1234",
+ "name": "Bob der Bausmeister"
+ },
+ "user": {
+ "email": "user@example.org",
+ "password": "1234",
+ "name": "Jenny Rostock"
+ }
+}
diff --git a/Human-Connection/cypress/integration/administration/TagsAndCategories.feature b/Human-Connection/cypress/integration/administration/TagsAndCategories.feature
new file mode 100644
index 000000000..e55535ea9
--- /dev/null
+++ b/Human-Connection/cypress/integration/administration/TagsAndCategories.feature
@@ -0,0 +1,37 @@
+Feature: Tags and Categories
+ As a database administrator
+ I would like to see a summary of all tags and categories and their usage
+ In order to be able to decide which tags and categories are popular or not
+
+ The currently deployed application, codename "Alpha", distinguishes between
+ categories and tags. Each post can have a number of categories and/or tags.
+ A few categories are required for each post, tags are completely optional.
+ Both help to find relevant posts in the database, e.g. users can filter for
+ categories.
+
+ If administrators summary of all tags and categories and how often they are
+ used, they learn what new category might be convenient for users, e.g. by
+ looking at the popularity of a tag.
+
+ Background:
+ Given my user account has the role "admin"
+ And we have a selection of tags and categories as well as posts
+ And I am logged in
+
+ Scenario: See an overview of categories
+ When I navigate to the administration dashboard
+ And I click on the menu item "Categories"
+ Then I can see the following table:
+ | | Name | Posts |
+ | | Just For Fun | 2 |
+ | | Happyness & Values | 1 |
+ | | Health & Wellbeing | 0 |
+
+ Scenario: See an overview of tags
+ When I navigate to the administration dashboard
+ And I click on the menu item "Tags"
+ Then I can see the following table:
+ | | Name | Users | Posts |
+ | 1 | Democracy | 3 | 4 |
+ | 2 | Nature | 2 | 3 |
+ | 3 | Ecology | 1 | 1 |
diff --git a/Human-Connection/cypress/integration/common/admin.js b/Human-Connection/cypress/integration/common/admin.js
new file mode 100644
index 000000000..346fe64fb
--- /dev/null
+++ b/Human-Connection/cypress/integration/common/admin.js
@@ -0,0 +1,21 @@
+import { When, Then } from 'cypress-cucumber-preprocessor/steps'
+
+/* global cy */
+
+When('I navigate to the administration dashboard', () => {
+ cy.get('.avatar-menu').click()
+ cy.get('.avatar-menu-popover')
+ .find('a[href="/admin"]')
+ .click()
+})
+
+Then('I can see the following table:', table => {
+ const headers = table.raw()[0]
+ headers.forEach((expected, i) => {
+ cy.get('thead th').eq(i).should('contain', expected)
+ })
+ const flattened = [].concat.apply([], table.rows())
+ flattened.forEach((expected, i) => {
+ cy.get('tbody td').eq(i).should('contain', expected)
+ })
+})
diff --git a/Human-Connection/cypress/integration/common/post.js b/Human-Connection/cypress/integration/common/post.js
new file mode 100644
index 000000000..814159a34
--- /dev/null
+++ b/Human-Connection/cypress/integration/common/post.js
@@ -0,0 +1,28 @@
+import { When, Then } from "cypress-cucumber-preprocessor/steps";
+
+const narratorAvatar =
+ "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg";
+
+Then("I click on the {string} button", text => {
+ cy.get("button")
+ .contains(text)
+ .click();
+});
+
+Then("my comment should be successfully created", () => {
+ cy.get(".iziToast-message").contains("Comment Submitted");
+});
+
+Then("I should see my comment", () => {
+ cy.get("div.comment p")
+ .should("contain", "Human Connection rocks")
+ .get(".ds-avatar img")
+ .should("have.attr", "src")
+ .and("contain", narratorAvatar)
+ .get("div p.ds-text span")
+ .should("contain", "today at");
+});
+
+Then("the editor should be cleared", () => {
+ cy.get(".ProseMirror p").should("have.class", "is-empty");
+});
diff --git a/Human-Connection/cypress/integration/common/profile.js b/Human-Connection/cypress/integration/common/profile.js
new file mode 100644
index 000000000..1df1e2652
--- /dev/null
+++ b/Human-Connection/cypress/integration/common/profile.js
@@ -0,0 +1,36 @@
+import { When, Then } from 'cypress-cucumber-preprocessor/steps'
+
+/* global cy */
+
+When('I visit my profile page', () => {
+ cy.openPage('profile/peter-pan')
+})
+
+Then('I should be able to change my profile picture', () => {
+ const avatarUpload = 'onourjourney.png'
+
+ cy.fixture(avatarUpload, 'base64').then(fileContent => {
+ cy.get('#customdropzone').upload(
+ { fileContent, fileName: avatarUpload, mimeType: 'image/png' },
+ { subjectType: 'drag-n-drop' }
+ )
+ })
+ cy.get('.profile-avatar img')
+ .should('have.attr', 'src')
+ .and('contains', 'onourjourney')
+ cy.contains('.iziToast-message', 'Upload successful').should(
+ 'have.length',
+ 1
+ )
+})
+
+When("I visit another user's profile page", () => {
+ cy.openPage('profile/peter-pan')
+})
+
+Then('I cannot upload a picture', () => {
+ cy.get('.ds-card-content')
+ .children()
+ .should('not.have.id', 'customdropzone')
+ .should('have.class', 'ds-avatar')
+})
diff --git a/Human-Connection/cypress/integration/common/report.js b/Human-Connection/cypress/integration/common/report.js
new file mode 100644
index 000000000..2c8b848b4
--- /dev/null
+++ b/Human-Connection/cypress/integration/common/report.js
@@ -0,0 +1,140 @@
+import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
+
+/* global cy */
+
+let lastReportTitle
+let davidIrvingPostTitle = 'The Truth about the Holocaust'
+let davidIrvingPostSlug = 'the-truth-about-the-holocaust'
+let davidIrvingName = 'David Irving'
+
+const savePostTitle = $post => {
+ return $post
+ .first()
+ .find('.ds-heading')
+ .first()
+ .invoke('text')
+ .then(title => {
+ lastReportTitle = title
+ })
+}
+
+Given("I see David Irving's post on the landing page", page => {
+ cy.openPage('landing')
+})
+
+Given("I see David Irving's post on the post page", page => {
+ cy.visit(`/post/${davidIrvingPostSlug}`)
+ cy.contains(davidIrvingPostTitle) // wait
+})
+
+Given('I am logged in with a {string} role', role => {
+ cy.factory().create('User', {
+ email: `${role}@example.org`,
+ password: '1234',
+ role
+ })
+ cy.login({
+ email: `${role}@example.org`,
+ password: '1234'
+ })
+})
+
+When('I click on "Report Post" from the content menu of the post', () => {
+ cy.contains('.ds-card', davidIrvingPostTitle)
+ .find('.content-menu-trigger')
+ .click({force: true})
+
+ cy.get('.popover .ds-menu-item-link')
+ .contains('Report Post')
+ .click()
+})
+
+When('I click on "Report User" from the content menu in the user info box', () => {
+ cy.contains('.ds-card', davidIrvingPostTitle)
+ .get('.user-content-menu .content-menu-trigger')
+ .click({ force: true })
+
+ cy.get('.popover .ds-menu-item-link')
+ .contains('Report User')
+ .click()
+})
+
+When('I click on the author', () => {
+ cy.get('.username')
+ .click()
+ .url().should('include', '/profile/')
+})
+
+When('I report the author', () => {
+ cy.get('.page-name-profile-id-slug').then(() => {
+ invokeReportOnElement('.ds-card').then(() => {
+ cy.get('button')
+ .contains('Send')
+ .click()
+ })
+ })
+})
+
+When('I click on send in the confirmation dialog', () => {
+ cy.get('button')
+ .contains('Send')
+ .click()
+})
+
+Then('I get a success message', () => {
+ cy.get('.iziToast-message').contains('Thanks')
+})
+
+Then('I see my reported user', () => {
+ cy.get('table').then(() => {
+ cy.get('tbody tr')
+ .first()
+ .contains(lastReportTitle.trim())
+ })
+})
+
+Then(`I can't see the moderation menu item`, () => {
+ cy.get('.avatar-menu-popover')
+ .find('a[href="/settings"]', 'Settings')
+ .should('exist') // OK, the dropdown is actually open
+
+ cy.get('.avatar-menu-popover')
+ .find('a[href="/moderation"]', 'Moderation')
+ .should('not.exist')
+})
+
+When(/^I confirm the reporting dialog .*:$/, message => {
+ cy.contains(message) // wait for element to become visible
+ cy.get('.ds-modal').within(() => {
+ cy.get('button')
+ .contains('Report')
+ .click()
+ })
+})
+
+Given('somebody reported the following posts:', table => {
+ table.hashes().forEach(({ id }) => {
+ const submitter = {
+ email: `submitter${id}@example.org`,
+ password: '1234'
+ }
+ cy.factory()
+ .create('User', submitter)
+ .authenticateAs(submitter)
+ .create('Report', {
+ id,
+ description: 'Offensive content'
+ })
+ })
+})
+
+Then('I see all the reported posts including the one from above', () => {
+ cy.get('table tbody').within(() => {
+ cy.contains('tr', davidIrvingPostTitle)
+ })
+})
+
+Then('each list item links to the post page', () => {
+ cy.contains(davidIrvingPostTitle).click()
+ cy.location('pathname').should('contain', '/post')
+})
diff --git a/Human-Connection/cypress/integration/common/search.js b/Human-Connection/cypress/integration/common/search.js
new file mode 100644
index 000000000..1c1981581
--- /dev/null
+++ b/Human-Connection/cypress/integration/common/search.js
@@ -0,0 +1,73 @@
+import { When, Then } from 'cypress-cucumber-preprocessor/steps'
+When('I search for {string}', value => {
+ cy.get('#nav-search')
+ .focus()
+ .type(value)
+})
+
+Then('I should have one post in the select dropdown', () => {
+ cy.get('.ds-select-dropdown').should($li => {
+ expect($li).to.have.length(1)
+ })
+})
+
+Then('I should see the following posts in the select dropdown:', table => {
+ table.hashes().forEach(({ title }) => {
+ cy.get('.ds-select-dropdown').should('contain', title)
+ })
+})
+
+When('I type {string} and press Enter', value => {
+ cy.get('#nav-search')
+ .focus()
+ .type(value)
+ .type('{enter}', { force: true })
+})
+
+When('I type {string} and press escape', value => {
+ cy.get('#nav-search')
+ .focus()
+ .type(value)
+ .type('{esc}')
+})
+
+Then('the search field should clear', () => {
+ cy.get('#nav-search').should('have.text', '')
+})
+
+When('I select an entry', () => {
+ cy.get('.ds-select-dropdown ul li')
+ .first()
+ .trigger('click')
+})
+
+Then("I should be on the post's page", () => {
+ cy.location('pathname').should(
+ 'contain',
+ '/post/'
+ )
+ cy.location('pathname').should(
+ 'eq',
+ '/post/p1/101-essays-that-will-change-the-way-you-think'
+ )
+})
+
+Then(
+ 'I should see posts with the searched-for term in the select dropdown',
+ () => {
+ cy.get('.ds-select-dropdown').should(
+ 'contain',
+ '101 Essays that will change the way you think'
+ )
+ }
+)
+
+Then(
+ 'I should not see posts without the searched-for term in the select dropdown',
+ () => {
+ cy.get('.ds-select-dropdown').should(
+ 'not.contain',
+ 'No searched for content'
+ )
+ }
+)
diff --git a/Human-Connection/cypress/integration/common/settings.js b/Human-Connection/cypress/integration/common/settings.js
new file mode 100644
index 000000000..664ffcff8
--- /dev/null
+++ b/Human-Connection/cypress/integration/common/settings.js
@@ -0,0 +1,123 @@
+import { When, Then } from 'cypress-cucumber-preprocessor/steps'
+
+/* global cy */
+
+let aboutMeText
+let myLocation
+
+const matchNameInUserMenu = name => {
+ cy.get('.avatar-menu').click() // open
+ cy.get('.avatar-menu-popover').contains(name)
+ cy.get('.avatar-menu').click() // close again
+}
+
+When('I save {string} as my new name', name => {
+ cy.get('input[id=name]')
+ .clear()
+ .type(name)
+ cy.get('[type=submit]')
+ .click()
+ .not('[disabled]')
+})
+
+When('I save {string} as my location', location => {
+ cy.get('input[id=city]').type(location)
+ cy.get('.ds-select-option')
+ .contains(location)
+ .click()
+ cy.get('[type=submit]')
+ .click()
+ .not('[disabled]')
+ myLocation = location
+})
+
+When('I have the following self-description:', text => {
+ cy.get('textarea[id=bio]')
+ .clear()
+ .type(text)
+ cy.get('[type=submit]')
+ .click()
+ .not('[disabled]')
+ aboutMeText = text
+})
+
+When('people visit my profile page', url => {
+ cy.openPage('/profile/peter-pan')
+})
+
+
+When('they can see the text in the info box below my avatar', () => {
+ cy.contains(aboutMeText)
+})
+
+Then('they can see the location in the info box below my avatar', () => {
+ cy.contains(myLocation)
+})
+
+Then('the name {string} is still there', name => {
+ matchNameInUserMenu(name)
+})
+
+Then(
+ 'I can see my new name {string} when I click on my profile picture in the top right',
+ name => matchNameInUserMenu(name)
+)
+
+When('I click on the {string} link', link => {
+ cy.get('a')
+ .contains(link)
+ .click()
+})
+
+Then('I should be on the {string} page', page => {
+ cy.location()
+ .should(loc => {
+ expect(loc.pathname).to.eq(page)
+ })
+ .get('h3')
+ .should('contain', 'Social media')
+})
+
+When('I add a social media link', () => {
+ cy.get("input[name='social-media']")
+ .type('https://freeradical.zone/peter-pan')
+ .get('button')
+ .contains('Add link')
+ .click()
+})
+
+Then('it gets saved successfully', () => {
+ cy.get('.iziToast-message')
+ .should('contain', 'Added social media')
+})
+
+Then('the new social media link shows up on the page', () => {
+ cy.get('a[href="https://freeradical.zone/peter-pan"]')
+ .should('have.length', 1)
+})
+
+Given('I have added a social media link', () => {
+ cy.openPage('/settings/my-social-media')
+ .get("input[name='social-media']")
+ .type('https://freeradical.zone/peter-pan')
+ .get('button')
+ .contains('Add link')
+ .click()
+})
+
+Then('they should be able to see my social media links', () => {
+ cy.get('.ds-card-content')
+ .contains('Where else can I find Peter Pan?')
+ .get('a[href="https://freeradical.zone/peter-pan"]')
+ .should('have.length', 1)
+})
+
+When('I delete a social media link', () => {
+ cy.get("a[name='delete']")
+ .click()
+})
+
+Then('it gets deleted successfully', () => {
+ cy.get('.iziToast-message')
+ .should('contain', 'Deleted social media')
+})
diff --git a/Human-Connection/cypress/integration/common/steps.js b/Human-Connection/cypress/integration/common/steps.js
new file mode 100644
index 000000000..73313d331
--- /dev/null
+++ b/Human-Connection/cypress/integration/common/steps.js
@@ -0,0 +1,361 @@
+import { Given, When, Then } from "cypress-cucumber-preprocessor/steps";
+import { getLangByName } from "../../support/helpers";
+
+/* global cy */
+
+let lastPost = {};
+
+let loginCredentials = {
+ email: "peterpan@example.org",
+ password: "1234"
+};
+const narratorParams = {
+ name: "Peter Pan",
+ avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
+ ...loginCredentials
+};
+
+Given("I am logged in", () => {
+ cy.login(loginCredentials);
+});
+
+Given("we have a selection of tags and categories as well as posts", () => {
+ cy.factory()
+ .authenticateAs(loginCredentials)
+ .create("Category", {
+ id: "cat1",
+ name: "Just For Fun",
+ slug: "justforfun",
+ icon: "smile"
+ })
+ .create("Category", {
+ id: "cat2",
+ name: "Happyness & Values",
+ slug: "happyness-values",
+ icon: "heart-o"
+ })
+ .create("Category", {
+ id: "cat3",
+ name: "Health & Wellbeing",
+ slug: "health-wellbeing",
+ icon: "medkit"
+ })
+ .create("Tag", { id: "t1", name: "Ecology" })
+ .create("Tag", { id: "t2", name: "Nature" })
+ .create("Tag", { id: "t3", name: "Democracy" });
+
+ const someAuthor = {
+ id: "authorId",
+ email: "author@example.org",
+ password: "1234"
+ };
+ const yetAnotherAuthor = {
+ id: "yetAnotherAuthor",
+ email: "yet-another-author@example.org",
+ password: "1234"
+ };
+ cy.factory()
+ .create("User", someAuthor)
+ .authenticateAs(someAuthor)
+ .create("Post", { id: "p0" })
+ .create("Post", { id: "p1" });
+ cy.factory()
+ .create("User", yetAnotherAuthor)
+ .authenticateAs(yetAnotherAuthor)
+ .create("Post", { id: "p2" });
+ cy.factory()
+ .authenticateAs(loginCredentials)
+ .create("Post", { id: "p3" })
+ .relate("Post", "Categories", { from: "p0", to: "cat1" })
+ .relate("Post", "Categories", { from: "p1", to: "cat2" })
+ .relate("Post", "Categories", { from: "p2", to: "cat1" })
+ .relate("Post", "Tags", { from: "p0", to: "t1" })
+ .relate("Post", "Tags", { from: "p0", to: "t2" })
+ .relate("Post", "Tags", { from: "p0", to: "t3" })
+ .relate("Post", "Tags", { from: "p1", to: "t2" })
+ .relate("Post", "Tags", { from: "p1", to: "t3" })
+ .relate("Post", "Tags", { from: "p2", to: "t2" })
+ .relate("Post", "Tags", { from: "p2", to: "t3" })
+ .relate("Post", "Tags", { from: "p3", to: "t3" });
+});
+
+Given("we have the following user accounts:", table => {
+ table.hashes().forEach(params => {
+ cy.factory().create("User", params);
+ });
+});
+
+Given("I have a user account", () => {
+ cy.factory().create("User", narratorParams);
+});
+
+Given("my user account has the role {string}", role => {
+ cy.factory().create("User", {
+ role,
+ ...loginCredentials
+ });
+});
+
+When("I log out", cy.logout);
+
+When("I visit {string}", page => {
+ cy.openPage(page);
+});
+
+When("I visit the {string} page", page => {
+ cy.openPage(page);
+});
+
+Given("I am on the {string} page", page => {
+ cy.openPage(page);
+});
+
+When("I fill in my email and password combination and click submit", () => {
+ cy.login(loginCredentials);
+});
+
+When(/(?:when )?I refresh the page/, () => {
+ cy.reload();
+});
+
+When("I log out through the menu in the top right corner", () => {
+ cy.get(".avatar-menu").click();
+ cy.get(".avatar-menu-popover")
+ .find('a[href="/logout"]')
+ .click();
+});
+
+Then("I can see my name {string} in the dropdown menu", () => {
+ cy.get(".avatar-menu-popover").should("contain", narratorParams.name);
+});
+
+Then("I see the login screen again", () => {
+ cy.location("pathname").should("contain", "/login");
+});
+
+Then("I can click on my profile picture in the top right corner", () => {
+ cy.get(".avatar-menu").click();
+ cy.get(".avatar-menu-popover");
+});
+
+Then("I am still logged in", () => {
+ cy.get(".avatar-menu").click();
+ cy.get(".avatar-menu-popover").contains(narratorParams.name);
+});
+
+When("I select {string} in the language menu", name => {
+ cy.switchLanguage(name, true);
+});
+Given("I previously switched the language to {string}", name => {
+ cy.switchLanguage(name, true);
+});
+Then("the whole user interface appears in {string}", name => {
+ const lang = getLangByName(name);
+ cy.get(`html[lang=${lang.code}]`);
+ cy.getCookie("locale").should("have.property", "value", lang.code);
+});
+Then("I see a button with the label {string}", label => {
+ cy.contains("button", label);
+});
+
+When(`I click on {string}`, linkOrButton => {
+ cy.contains(linkOrButton).click();
+});
+
+When(`I click on the menu item {string}`, linkOrButton => {
+ cy.contains(".ds-menu-item", linkOrButton).click();
+});
+
+When("I press {string}", label => {
+ cy.contains(label).click();
+});
+
+Given("we have the following posts in our database:", table => {
+ table.hashes().forEach(({ Author, ...postAttributes }) => {
+ const userAttributes = {
+ name: Author,
+ email: `${Author}@example.org`,
+ password: "1234"
+ };
+ postAttributes.deleted = Boolean(postAttributes.deleted);
+ const disabled = Boolean(postAttributes.disabled);
+ cy.factory()
+ .create("User", userAttributes)
+ .authenticateAs(userAttributes)
+ .create("Post", postAttributes);
+ if (disabled) {
+ const moderatorParams = {
+ email: "moderator@example.org",
+ role: "moderator",
+ password: "1234"
+ };
+ cy.factory()
+ .create("User", moderatorParams)
+ .authenticateAs(moderatorParams)
+ .mutate("mutation($id: ID!) { disable(id: $id) }", postAttributes);
+ }
+ });
+});
+
+Then("I see a success message:", message => {
+ cy.contains(message);
+});
+
+When("I click on the avatar menu in the top right corner", () => {
+ cy.get(".avatar-menu").click();
+});
+
+When(
+ "I click on the big plus icon in the bottom right corner to create post",
+ () => {
+ cy.get(".post-add-button").click();
+ }
+);
+
+Given("I previously created a post", () => {
+ lastPost.title = "previously created post";
+ lastPost.content = "with some content";
+ cy.factory()
+ .authenticateAs(loginCredentials)
+ .create("Post", lastPost);
+});
+
+When("I choose {string} as the title of the post", title => {
+ lastPost.title = title.replace("\n", " ");
+ cy.get('input[name="title"]').type(lastPost.title);
+});
+
+When("I type in the following text:", text => {
+ lastPost.content = text.replace("\n", " ");
+ cy.get(".ProseMirror").type(lastPost.content);
+});
+
+Then("the post shows up on the landing page at position {int}", index => {
+ cy.openPage("landing");
+ const selector = `.post-card:nth-child(${index}) > .ds-card-content`;
+ cy.get(selector).should("contain", lastPost.title);
+ cy.get(selector).should("contain", lastPost.content);
+});
+
+Then("I get redirected to {string}", route => {
+ cy.location("pathname").should("contain", route.replace("...", ""));
+});
+
+Then("the post was saved successfully", () => {
+ cy.get(".ds-card-content > .ds-heading").should("contain", lastPost.title);
+ cy.get(".content").should("contain", lastPost.content);
+});
+
+Then(/^I should see only ([0-9]+) posts? on the landing page/, postCount => {
+ cy.get(".post-card").should("have.length", postCount);
+});
+
+Then("the first post on the landing page has the title:", title => {
+ cy.get(".post-card:first").should("contain", title);
+});
+
+Then(
+ "the page {string} returns a 404 error with a message:",
+ (route, message) => {
+ // TODO: how can we check HTTP codes with cypress?
+ cy.visit(route, { failOnStatusCode: false });
+ cy.get(".error").should("contain", message);
+ }
+);
+
+Given("my user account has the following login credentials:", table => {
+ loginCredentials = table.hashes()[0];
+ cy.debug();
+ cy.factory().create("User", loginCredentials);
+});
+
+When("I fill the password form with:", table => {
+ table = table.rowsHash();
+ cy.get("input[id=oldPassword]")
+ .type(table["Your old password"])
+ .get("input[id=newPassword]")
+ .type(table["Your new passsword"])
+ .get("input[id=confirmPassword]")
+ .type(table["Confirm new password"]);
+});
+
+When("submit the form", () => {
+ cy.get("form").submit();
+});
+
+Then("I cannot login anymore with password {string}", password => {
+ cy.reload();
+ const { email } = loginCredentials;
+ cy.visit(`/login`);
+ cy.get("input[name=email]")
+ .trigger("focus")
+ .type(email);
+ cy.get("input[name=password]")
+ .trigger("focus")
+ .type(password);
+ cy.get("button[name=submit]")
+ .as("submitButton")
+ .click();
+ cy.get(".iziToast-wrapper").should(
+ "contain",
+ "Incorrect email address or password."
+ );
+});
+
+Then("I can login successfully with password {string}", password => {
+ cy.reload();
+ cy.login({
+ ...loginCredentials,
+ ...{ password }
+ });
+ cy.get(".iziToast-wrapper").should("contain", "You are logged in!");
+});
+
+When("I log in with the following credentials:", table => {
+ const { email, password } = table.hashes()[0];
+ cy.login({ email, password });
+});
+
+When("open the notification menu and click on the first item", () => {
+ cy.get(".notifications-menu").click();
+ cy.get(".notification-mention-post")
+ .first()
+ .click();
+});
+
+Then("see {int} unread notifications in the top menu", count => {
+ cy.get(".notifications-menu").should("contain", count);
+});
+
+Then("I get to the post page of {string}", path => {
+ path = path.replace("...", "");
+ cy.url().should("contain", "/post/");
+ cy.url().should("contain", path);
+});
+
+When(
+ "I start to write a new post with the title {string} beginning with:",
+ (title, intro) => {
+ cy.get(".post-add-button").click();
+ cy.get('input[name="title"]').type(title);
+ cy.get(".ProseMirror").type(intro);
+ }
+);
+
+When("mention {string} in the text", mention => {
+ cy.get(".ProseMirror").type(" @");
+ cy.get(".suggestion-list__item")
+ .contains(mention)
+ .click();
+ cy.debug();
+});
+
+Then("the notification gets marked as read", () => {
+ cy.get(".notification")
+ .first()
+ .should("have.class", "read");
+});
+
+Then("there are no notifications in the top menu", () => {
+ cy.get(".notifications-menu").should("contain", "0");
+});
diff --git a/Human-Connection/cypress/integration/internationalization/Internationalization.feature b/Human-Connection/cypress/integration/internationalization/Internationalization.feature
new file mode 100644
index 000000000..0a5f90ff0
--- /dev/null
+++ b/Human-Connection/cypress/integration/internationalization/Internationalization.feature
@@ -0,0 +1,23 @@
+Feature: Internationalization
+ As a user who is not very fluent in English
+ I would like to see the user interface translated to my preferred language
+ In order to be able to understand the interface
+
+ Background:
+ Given I am on the "login" page
+
+ Scenario Outline: I select "" in the language menu and see ""
+ When I select "" in the language menu
+ Then the whole user interface appears in ""
+ Then I see a button with the label ""
+
+ Examples: Login Button
+ | language | buttonLabel |
+ | Français | Connexion |
+ | Deutsch | Einloggen |
+ | English | Login |
+
+ Scenario: Keep preferred language after refresh
+ Given I previously switched the language to "Français"
+ And I refresh the page
+ Then the whole user interface appears in "Français"
diff --git a/Human-Connection/cypress/integration/moderation/HidePosts.feature b/Human-Connection/cypress/integration/moderation/HidePosts.feature
new file mode 100644
index 000000000..e886e5f95
--- /dev/null
+++ b/Human-Connection/cypress/integration/moderation/HidePosts.feature
@@ -0,0 +1,26 @@
+Feature: Hide Posts
+ As the moderator team
+ we'd like to be able to hide posts from the public
+ to enforce our network's code of conduct and/or legal regulations
+
+ Background:
+ Given we have the following posts in our database:
+ | id | title | deleted | disabled |
+ | p1 | This post should be visible | | |
+ | p2 | This post is disabled | | x |
+ | p3 | This post is deleted | x | |
+
+ Scenario: Disabled posts don't show up on the landing page
+ Given I am logged in with a "user" role
+ Then I should see only 1 post on the landing page
+ And the first post on the landing page has the title:
+ """
+ This post should be visible
+ """
+
+ Scenario: Visiting a disabled post's page should return 404
+ Given I am logged in with a "user" role
+ Then the page "/post/this-post-is-disabled" returns a 404 error with a message:
+ """
+ This post could not be found
+ """
diff --git a/Human-Connection/cypress/integration/moderation/ReportContent.feature b/Human-Connection/cypress/integration/moderation/ReportContent.feature
new file mode 100644
index 000000000..62fb4f421
--- /dev/null
+++ b/Human-Connection/cypress/integration/moderation/ReportContent.feature
@@ -0,0 +1,60 @@
+Feature: Report and Moderate
+ As a user
+ I would like to report content that violates the community guidlines
+ So the moderators can take action on it
+
+ As a moderator
+ I would like to see all reported content
+ So I can look into it and decide what to do
+
+ Background:
+ Given we have the following posts in our database:
+ | Author | id | title | content |
+ | David Irving | p1 | The Truth about the Holocaust | It never existed! |
+
+
+ Scenario Outline: Report a post from various pages
+ Given I am logged in with a "user" role
+ When I see David Irving's post on the
+ And I click on "Report Post" from the content menu of the post
+ And I confirm the reporting dialog because it is a criminal act under German law:
+ """
+ Do you really want to report the contribution "The Truth about the Holocaust"?
+ """
+ Then I see a success message:
+ """
+ Thanks for reporting!
+ """
+ Examples:
+ | Page |
+ | landing page |
+ | post page |
+
+ Scenario: Report user
+ Given I am logged in with a "user" role
+ And I see David Irving's post on the post page
+ When I click on the author
+ And I click on "Report User" from the content menu in the user info box
+ And I confirm the reporting dialog because he is a holocaust denier:
+ """
+ Do you really want to report the user "David Irving"?
+ """
+ Then I see a success message:
+ """
+ Thanks for reporting!
+ """
+
+ Scenario: Review reported content
+ Given somebody reported the following posts:
+ | id |
+ | p1 |
+ And I am logged in with a "moderator" role
+ When I click on the avatar menu in the top right corner
+ And I click on "Moderation"
+ Then I see all the reported posts including the one from above
+ And each list item links to the post page
+
+ Scenario: Normal user can't see the moderation page
+ Given I am logged in with a "user" role
+ When I click on the avatar menu in the top right corner
+ Then I can't see the moderation menu item
diff --git a/Human-Connection/cypress/integration/notifications/Mentions.feature b/Human-Connection/cypress/integration/notifications/Mentions.feature
new file mode 100644
index 000000000..28f7cf456
--- /dev/null
+++ b/Human-Connection/cypress/integration/notifications/Mentions.feature
@@ -0,0 +1,31 @@
+Feature: Notifications for a mentions
+ As a user
+ I want to be notified if sb. mentions me in a post or comment
+ In order join conversations about or related to me
+
+ Background:
+ Given we have the following user accounts:
+ | name | slug | email | password |
+ | Wolle aus Hamburg | wolle-aus-hamburg | wolle@example.org | 1234 |
+ | Matt Rider | matt-rider | matt@example.org | 4321 |
+
+ Scenario: Mention another user, re-login as this user and see notifications
+ Given I log in with the following credentials:
+ | email | password |
+ | wolle@example.org | 1234 |
+ And I start to write a new post with the title "Hey Matt" beginning with:
+ """
+ Big shout to our fellow contributor
+ """
+ And mention "@matt-rider" in the text
+ And I click on "Save"
+ When I log out
+ And I log in with the following credentials:
+ | email | password |
+ | matt@example.org | 4321 |
+ And see 1 unread notifications in the top menu
+ And open the notification menu and click on the first item
+ Then I get to the post page of ".../hey-matt"
+ And the notification gets marked as read
+ But when I refresh the page
+ Then there are no notifications in the top menu
diff --git a/Human-Connection/cypress/integration/post/Comment.feature b/Human-Connection/cypress/integration/post/Comment.feature
new file mode 100644
index 000000000..e7e462824
--- /dev/null
+++ b/Human-Connection/cypress/integration/post/Comment.feature
@@ -0,0 +1,22 @@
+Feature: Post Comment
+ As a user
+ I want to comment on contributions of others
+ To be able to express my thoughts and emotions about these, discuss, and add give further information.
+
+ Background:
+ Given we have the following posts in our database:
+ | id | title | slug |
+ | bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays |
+ And I have a user account
+ And I am logged in
+
+ Scenario: Comment creation
+ Given I visit "post/bWBjpkTKZp/101-essays"
+ And I type in the following text:
+ """
+ Human Connection rocks
+ """
+ And I click on the "Comment" button
+ Then my comment should be successfully created
+ And I should see my comment
+ And the editor should be cleared
diff --git a/Human-Connection/cypress/integration/post/PersistentLinks.feature b/Human-Connection/cypress/integration/post/PersistentLinks.feature
new file mode 100644
index 000000000..5ea48ef6a
--- /dev/null
+++ b/Human-Connection/cypress/integration/post/PersistentLinks.feature
@@ -0,0 +1,41 @@
+Feature: Persistent Links
+ As a user
+ I want all links to carry permanent information that identifies the linked resource
+ In order to have persistent links even if a part of the URL might change
+
+ | | Modifiable | Referenceable | Unique | Purpose |
+ | -- | -- | -- | -- | -- |
+ | ID | no | yes | yes | Identity, Traceability, Links |
+ | Slug | yes | yes | yes | @-Mentions, SEO-friendly URL |
+ | Name | yes | no | no | Search, self-description |
+
+
+ Background:
+ Given we have the following user accounts:
+ | id | name | slug |
+ | MHNqce98y1 | Stephen Hawking | thehawk |
+ And we have the following posts in our database:
+ | id | title | slug |
+ | bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays |
+ And I have a user account
+ And I am logged in
+
+ Scenario Outline: Link with slug only is valid and gets auto-completed
+ When I visit ""
+ Then I get redirected to ""
+ Examples:
+ | url | redirectUrl |
+ | /profile/thehawk | /profile/MHNqce98y1/thehawk |
+ | /post/101-essays | /post/bWBjpkTKZp/101-essays |
+
+ Scenario: Link with id only will always point to the same user
+ When I visit "/profile/MHNqce98y1"
+ Then I get redirected to "/profile/MHNqce98y1/thehawk"
+
+ Scenario Outline: ID takes precedence over slug
+ When I visit ""
+ Then I get redirected to ""
+ Examples:
+ | url | redirectUrl |
+ | /profile/MHNqce98y1/stephen-hawking | /profile/MHNqce98y1/thehawk |
+ | /post/bWBjpkTKZp/the-way-you-think | /post/bWBjpkTKZp/101-essays |
diff --git a/Human-Connection/cypress/integration/post/WritePost.feature b/Human-Connection/cypress/integration/post/WritePost.feature
new file mode 100644
index 000000000..06ac4a175
--- /dev/null
+++ b/Human-Connection/cypress/integration/post/WritePost.feature
@@ -0,0 +1,25 @@
+Feature: Create a post
+ As a user
+ I would like to create a post
+ To say something to everyone in the community
+
+ Background:
+ Given I have a user account
+ And I am logged in
+ And I am on the "landing" page
+
+ Scenario: Create a post
+ When I click on the big plus icon in the bottom right corner to create post
+ And I choose "My first post" as the title of the post
+ And I type in the following text:
+ """
+ Human Connection is a free and open-source social network
+ for active citizenship.
+ """
+ And I click on "Save"
+ Then I get redirected to ".../my-first-post"
+ And the post was saved successfully
+
+ Scenario: See a post on the landing page
+ Given I previously created a post
+ Then the post shows up on the landing page at position 1
diff --git a/Human-Connection/cypress/integration/search/Search.feature b/Human-Connection/cypress/integration/search/Search.feature
new file mode 100644
index 000000000..71aee608a
--- /dev/null
+++ b/Human-Connection/cypress/integration/search/Search.feature
@@ -0,0 +1,41 @@
+Feature: Search
+ As a user
+ I would like to be able to search for specific words
+ In order to find related content
+
+ Background:
+ Given I have a user account
+ And we have the following posts in our database:
+ | Author | id | title | content |
+ | Brianna Wiest | p1 | 101 Essays that will change the way you think | 101 Essays, of course! |
+ | Brianna Wiest | p2 | No searched for content | will be found in this post, I guarantee |
+ Given I am logged in
+
+ Scenario: Search for specific words
+ When I search for "Essays"
+ Then I should have one post in the select dropdown
+ Then I should see the following posts in the select dropdown:
+ | title |
+ | 101 Essays that will change the way you think |
+
+ Scenario: Press enter starts search
+ When I type "Essa" and press Enter
+ Then I should have one post in the select dropdown
+ Then I should see the following posts in the select dropdown:
+ | title |
+ | 101 Essays that will change the way you think |
+
+ Scenario: Press escape clears search
+ When I type "Ess" and press escape
+ Then the search field should clear
+
+ Scenario: Select entry goes to post
+ When I search for "Essays"
+ And I select an entry
+ Then I should be on the post's page
+
+ Scenario: Select dropdown content
+ When I search for "Essays"
+ Then I should have one post in the select dropdown
+ Then I should see posts with the searched-for term in the select dropdown
+ And I should not see posts without the searched-for term in the select dropdown
diff --git a/Human-Connection/cypress/integration/user_account/ChangePassword.feature b/Human-Connection/cypress/integration/user_account/ChangePassword.feature
new file mode 100644
index 000000000..44e4e5483
--- /dev/null
+++ b/Human-Connection/cypress/integration/user_account/ChangePassword.feature
@@ -0,0 +1,31 @@
+Feature: Change password
+ As a user
+ I want to change my password in my settings
+ For security, e.g. if I exposed my password by accident
+
+ Login via email and password is a well-known authentication procedure and you
+ can assure to the server that you are who you claim to be. Either if you
+ exposed your password by acccident and you want to invalidate the exposed
+ password or just out of an good habit, you want to change your password.
+
+ Background:
+ Given my user account has the following login credentials:
+ | email | password |
+ | user@example.org | exposed |
+ And I am logged in
+
+ Scenario: Change my password
+ Given I am on the "settings" page
+ And I click on "Security"
+ When I fill the password form with:
+ | Your old password | exposed |
+ | Your new passsword | secure |
+ | Confirm new password | secure |
+ And submit the form
+ And I see a success message:
+ """
+ Password successfully changed!
+ """
+ And I log out through the menu in the top right corner
+ Then I cannot login anymore with password "exposed"
+ But I can login successfully with password "secure"
diff --git a/Human-Connection/cypress/integration/user_account/Login.feature b/Human-Connection/cypress/integration/user_account/Login.feature
new file mode 100644
index 000000000..3837f7042
--- /dev/null
+++ b/Human-Connection/cypress/integration/user_account/Login.feature
@@ -0,0 +1,23 @@
+Feature: Authentication
+ As a database administrator
+ I want users to sign in
+ In order to attribute posts and other contributions to their authors
+
+ Background:
+ Given I have a user account
+
+ Scenario: Log in
+ When I visit the "/login" page
+ And I fill in my email and password combination and click submit
+ Then I can click on my profile picture in the top right corner
+ And I can see my name "Peter Lustig" in the dropdown menu
+
+ Scenario: Refresh and stay logged in
+ Given I am logged in
+ When I refresh the page
+ Then I am still logged in
+
+ Scenario: Log out
+ Given I am logged in
+ When I log out through the menu in the top right corner
+ Then I see the login screen again
diff --git a/Human-Connection/cypress/integration/user_profile/AboutMeAndLocation.feature b/Human-Connection/cypress/integration/user_profile/AboutMeAndLocation.feature
new file mode 100644
index 000000000..2a512bf3f
--- /dev/null
+++ b/Human-Connection/cypress/integration/user_profile/AboutMeAndLocation.feature
@@ -0,0 +1,37 @@
+Feature: About me and location
+ As a user
+ I would like to add some about me text and a location
+ So others can get some info about me and my location
+
+ The location and about me are displayed on the user profile. Later it will be possible
+ to search for users by location.
+
+ Background:
+ Given I have a user account
+ And I am logged in
+ And I am on the "settings" page
+
+ Scenario: Change username
+ When I save "Hansi" as my new name
+ Then I can see my new name "Hansi" when I click on my profile picture in the top right
+ And when I refresh the page
+ Then the name "Hansi" is still there
+
+ Scenario Outline: I set my location to ""
+ When I save "" as my location
+ When people visit my profile page
+ Then they can see the location in the info box below my avatar
+
+ Examples: Location
+ | location | type |
+ | Paris | City |
+ | Saxony-Anhalt | Region |
+ | Germany | Country |
+
+ Scenario: Display a description on profile page
+ Given I have the following self-description:
+ """
+ Ich lebe fettlos, fleischlos, fischlos dahin, fühle mich aber ganz wohl dabei
+ """
+ When people visit my profile page
+ Then they can see the text in the info box below my avatar
diff --git a/Human-Connection/cypress/integration/user_profile/SocialMedia.feature b/Human-Connection/cypress/integration/user_profile/SocialMedia.feature
new file mode 100644
index 000000000..d21167c6b
--- /dev/null
+++ b/Human-Connection/cypress/integration/user_profile/SocialMedia.feature
@@ -0,0 +1,29 @@
+Feature: List Social Media Accounts
+ As a User
+ I'd like to enter my social media
+ So I can show them to other users to get in contact
+
+ Background:
+ Given I have a user account
+ And I am logged in
+
+ Scenario: Adding Social Media
+ Given I am on the "settings" page
+ And I click on the "Social media" link
+ Then I should be on the "/settings/my-social-media" page
+ When I add a social media link
+ Then it gets saved successfully
+ And the new social media link shows up on the page
+
+ Scenario: Other user's viewing my Social Media
+ Given I have added a social media link
+ When people visit my profile page
+ Then they should be able to see my social media links
+
+ Scenario: Deleting Social Media
+ Given I am on the "settings" page
+ And I click on the "Social media" link
+ Then I should be on the "/settings/my-social-media" page
+ Given I have added a social media link
+ When I delete a social media link
+ Then it gets deleted successfully
diff --git a/Human-Connection/cypress/integration/user_profile/UploadUserProfileImage.feature b/Human-Connection/cypress/integration/user_profile/UploadUserProfileImage.feature
new file mode 100644
index 000000000..b46a31de8
--- /dev/null
+++ b/Human-Connection/cypress/integration/user_profile/UploadUserProfileImage.feature
@@ -0,0 +1,18 @@
+Feature: Upload UserProfile Image
+ As a user
+ I would like to be able to add an avatar/profile pic to my profile
+ So that I can personalize my profile
+
+
+ Background:
+ Given I have a user account
+
+ Scenario: Change my UserProfile Image
+ Given I am logged in
+ And I visit my profile page
+ Then I should be able to change my profile picture
+
+ Scenario: Unable to change another user's avatar
+ Given I am logged in with a "user" role
+ And I visit another user's profile page
+ Then I cannot upload a picture
\ No newline at end of file
diff --git a/Human-Connection/cypress/plugins/index.js b/Human-Connection/cypress/plugins/index.js
new file mode 100644
index 000000000..4ec4addb3
--- /dev/null
+++ b/Human-Connection/cypress/plugins/index.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+const cucumber = require('cypress-cucumber-preprocessor').default
+module.exports = on => {
+ // (on, config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+ on('file:preprocessor', cucumber())
+}
diff --git a/Human-Connection/cypress/support/commands.js b/Human-Connection/cypress/support/commands.js
new file mode 100644
index 000000000..f6253af20
--- /dev/null
+++ b/Human-Connection/cypress/support/commands.js
@@ -0,0 +1,76 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+
+/* globals Cypress cy */
+import 'cypress-file-upload'
+import { getLangByName } from './helpers'
+import users from '../fixtures/users.json'
+
+const switchLang = name => {
+ cy.get('.locale-menu').click()
+ cy.contains('.locale-menu-popover a', name).click()
+}
+
+Cypress.Commands.add('switchLanguage', (name, force) => {
+ const code = getLangByName(name).code
+ if (force) {
+ switchLang(name)
+ } else {
+ cy.get('html').then($html => {
+ if ($html && $html.attr('lang') !== code) {
+ switchLang(name)
+ }
+ })
+ }
+})
+
+Cypress.Commands.add('login', ({ email, password }) => {
+ cy.visit(`/login`)
+ cy.get('input[name=email]')
+ .trigger('focus')
+ .type(email)
+ cy.get('input[name=password]')
+ .trigger('focus')
+ .type(password)
+ cy.get('button[name=submit]')
+ .as('submitButton')
+ .click()
+ cy.get('.iziToast-message').should('contain', 'You are logged in!')
+ cy.get('.iziToast-close').click()
+})
+
+Cypress.Commands.add('logout', (email, password) => {
+ cy.visit(`/logout`)
+ cy.location('pathname').should('contain', '/login') // we're out
+})
+
+Cypress.Commands.add('openPage', page => {
+ if (page === 'landing') {
+ page = ''
+ }
+ cy.visit(`/${page}`)
+})
+
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This is will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
diff --git a/Human-Connection/cypress/support/factories.js b/Human-Connection/cypress/support/factories.js
new file mode 100644
index 000000000..3bdb86800
--- /dev/null
+++ b/Human-Connection/cypress/support/factories.js
@@ -0,0 +1,50 @@
+import Factory from '../../backend/src/seed/factories'
+import { getDriver } from '../../backend/src/bootstrap/neo4j'
+
+const neo4jDriver = getDriver({
+ uri: Cypress.env('NEO4J_URI'),
+ username: Cypress.env('NEO4J_USERNAME'),
+ password: Cypress.env('NEO4J_PASSWORD')
+})
+const factory = Factory({ neo4jDriver })
+const seedServerHost = Cypress.env('SEED_SERVER_HOST')
+
+beforeEach(async () => {
+ await factory.cleanDatabase({ seedServerHost, neo4jDriver })
+})
+
+Cypress.Commands.add('factory', () => {
+ return Factory({ seedServerHost })
+})
+
+Cypress.Commands.add(
+ 'create',
+ { prevSubject: true },
+ (factory, node, properties) => {
+ return factory.create(node, properties)
+ }
+)
+
+Cypress.Commands.add(
+ 'relate',
+ { prevSubject: true },
+ (factory, node, relationship, properties) => {
+ return factory.relate(node, relationship, properties)
+ }
+)
+
+Cypress.Commands.add(
+ 'mutate',
+ { prevSubject: true },
+ (factory, mutation, variables) => {
+ return factory.mutate(mutation, variables)
+ }
+)
+
+Cypress.Commands.add(
+ 'authenticateAs',
+ { prevSubject: true },
+ (factory, loginCredentials) => {
+ return factory.authenticateAs(loginCredentials)
+ }
+)
diff --git a/Human-Connection/cypress/support/helpers.js b/Human-Connection/cypress/support/helpers.js
new file mode 100644
index 000000000..4a8376ec0
--- /dev/null
+++ b/Human-Connection/cypress/support/helpers.js
@@ -0,0 +1,10 @@
+import find from 'lodash/find'
+
+const helpers = {
+ locales: require('../../webapp/locales'),
+ getLangByName: name => {
+ return find(helpers.locales, { name })
+ }
+}
+
+export default helpers
diff --git a/Human-Connection/cypress/support/index.js b/Human-Connection/cypress/support/index.js
new file mode 100644
index 000000000..195b0de7d
--- /dev/null
+++ b/Human-Connection/cypress/support/index.js
@@ -0,0 +1,26 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+
+import './commands'
+import './factories'
+
+// intermittent failing tests
+import 'cypress-plugin-retries'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
diff --git a/Human-Connection/deployment/.gitignore b/Human-Connection/deployment/.gitignore
new file mode 100644
index 000000000..14cfa18ed
--- /dev/null
+++ b/Human-Connection/deployment/.gitignore
@@ -0,0 +1,4 @@
+secrets.yaml
+configmap.yaml
+**/secrets.yaml
+**/configmap.yaml
diff --git a/Human-Connection/deployment/README.md b/Human-Connection/deployment/README.md
new file mode 100644
index 000000000..0615ccf9b
--- /dev/null
+++ b/Human-Connection/deployment/README.md
@@ -0,0 +1,11 @@
+# Human-Connection Nitro \| Deployment Configuration
+
+We deploy with [kubernetes](https://kubernetes.io/). In order to deploy your own
+network you have to [install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
+and get a kubernetes cluster.
+
+We have tested two different kubernetes providers: [Minikube](./minikube/README.md)
+and [Digital Ocean](./digital-ocean/README.md).
+
+Check out the specific documentation for your provider. After that, learn how
+to apply the specific kubernetes configuration for [Human Connection](./human-connection/README.md).
diff --git a/Human-Connection/deployment/digital-ocean/README.md b/Human-Connection/deployment/digital-ocean/README.md
new file mode 100644
index 000000000..12c272691
--- /dev/null
+++ b/Human-Connection/deployment/digital-ocean/README.md
@@ -0,0 +1,26 @@
+# Digital Ocean
+
+As a start, read the [introduction into kubernetes](https://www.digitalocean.com/community/tutorials/an-introduction-to-kubernetes) by the folks at Digital Ocean. The following section should enable you to deploy Human Connection to your kubernetes cluster.
+
+## Connect to your local cluster
+
+1. Create a cluster at [Digital Ocean](https://www.digitalocean.com/).
+2. Download the `***-kubeconfig.yaml` from the Web UI.
+3. Move the file to the default location where kubectl expects it to be: `mv ***-kubeconfig.yaml ~/.kube/config`. Alternatively you can set the config on every command: `--kubeconfig ***-kubeconfig.yaml`
+4. Now check if you can connect to the cluster and if its your newly created one by running: `kubectl get nodes`
+
+The output should look about like this:
+```
+$ kubectl get nodes
+NAME STATUS ROLES AGE VERSION
+nifty-driscoll-uu1w Ready 69d v1.13.2
+nifty-driscoll-uuiw Ready 69d v1.13.2
+nifty-driscoll-uusn Ready 69d v1.13.2
+```
+
+If you got the steps right above and see your nodes you can continue.
+
+Digital Ocean kubernetes clusters don't have a graphical interface, so I suggest
+to setup the [kubernetes dashboard](./dashboard/README.md) as a next step.
+Configuring [HTTPS](./https/README.md) is bit tricky and therefore I suggest to
+do this as a last step.
diff --git a/Human-Connection/deployment/digital-ocean/dashboard/README.md b/Human-Connection/deployment/digital-ocean/dashboard/README.md
new file mode 100644
index 000000000..3ae6378bf
--- /dev/null
+++ b/Human-Connection/deployment/digital-ocean/dashboard/README.md
@@ -0,0 +1,55 @@
+# Install Kubernetes Dashboard
+
+The kubernetes dashboard is optional but very helpful for debugging. If you want to install it, you have to do so only **once** per cluster:
+
+```bash
+# in folder deployment/digital-ocean/
+$ kubectl apply -f dashboard/
+$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended/kubernetes-dashboard.yaml
+```
+
+### Login to your dashboard
+
+Proxy the remote kubernetes dashboard to localhost:
+
+```bash
+$ kubectl proxy
+```
+
+Visit:
+
+[http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/](http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/)
+
+You should see a login screen.
+
+To get your token for the dashboard you can run this command:
+
+```bash
+$ kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}')
+```
+
+It should print something like:
+
+```text
+Name: admin-user-token-6gl6l
+Namespace: kube-system
+Labels:
+Annotations: kubernetes.io/service-account.name=admin-user
+ kubernetes.io/service-account.uid=b16afba9-dfec-11e7-bbb9-901b0e532516
+
+Type: kubernetes.io/service-account-token
+
+Data
+====
+ca.crt: 1025 bytes
+namespace: 11 bytes
+token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi11c2VyLXRva2VuLTZnbDZsIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluLXVzZXIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJiMTZhZmJhOS1kZmVjLTExZTctYmJiOS05MDFiMGU1MzI1MTYiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06YWRtaW4tdXNlciJ9.M70CU3lbu3PP4OjhFms8PVL5pQKj-jj4RNSLA4YmQfTXpPUuxqXjiTf094_Rzr0fgN_IVX6gC4fiNUL5ynx9KU-lkPfk0HnX8scxfJNzypL039mpGt0bbe1IXKSIRaq_9VW59Xz-yBUhycYcKPO9RM2Qa1Ax29nqNVko4vLn1_1wPqJ6XSq3GYI8anTzV8Fku4jasUwjrws6Cn6_sPEGmL54sq5R4Z5afUtv-mItTmqZZdxnkRqcJLlg2Y8WbCPogErbsaCDJoABQ7ppaqHetwfM_0yMun6ABOQbIwwl8pspJhpplKwyo700OSpvTT9zlBsu-b35lzXGBRHzv5g_RA
+```
+
+Grab the token from above and paste it into the [login screen](http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/)
+
+When you are logged in, you should see sth. like:
+
+
+
+Feel free to save the login token from above in your password manager. Unlike the `kubeconfig` file, this token does not expire.
diff --git a/Human-Connection/deployment/digital-ocean/dashboard/admin-user.yaml b/Human-Connection/deployment/digital-ocean/dashboard/admin-user.yaml
new file mode 100644
index 000000000..27b6bb802
--- /dev/null
+++ b/Human-Connection/deployment/digital-ocean/dashboard/admin-user.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: admin-user
+ namespace: kube-system
diff --git a/Human-Connection/deployment/digital-ocean/dashboard/dashboard-screenshot.png b/Human-Connection/deployment/digital-ocean/dashboard/dashboard-screenshot.png
new file mode 100644
index 000000000..6aefb5414
Binary files /dev/null and b/Human-Connection/deployment/digital-ocean/dashboard/dashboard-screenshot.png differ
diff --git a/Human-Connection/deployment/digital-ocean/dashboard/role-binding.yaml b/Human-Connection/deployment/digital-ocean/dashboard/role-binding.yaml
new file mode 100644
index 000000000..faa8927a2
--- /dev/null
+++ b/Human-Connection/deployment/digital-ocean/dashboard/role-binding.yaml
@@ -0,0 +1,12 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: admin-user
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+subjects:
+- kind: ServiceAccount
+ name: admin-user
+ namespace: kube-system
diff --git a/Human-Connection/deployment/digital-ocean/https/.gitignore b/Human-Connection/deployment/digital-ocean/https/.gitignore
new file mode 100644
index 000000000..bebae8d05
--- /dev/null
+++ b/Human-Connection/deployment/digital-ocean/https/.gitignore
@@ -0,0 +1,2 @@
+ingress.yaml
+issuer.yaml
diff --git a/Human-Connection/deployment/digital-ocean/https/README.md b/Human-Connection/deployment/digital-ocean/https/README.md
new file mode 100644
index 000000000..d100ba8dd
--- /dev/null
+++ b/Human-Connection/deployment/digital-ocean/https/README.md
@@ -0,0 +1,68 @@
+# Setup Ingress and HTTPS
+
+Follow [this quick start guide](https://docs.cert-manager.io/en/latest/tutorials/acme/quick-start/index.html) and install certmanager via helm and tiller:
+
+```text
+$ kubectl create serviceaccount tiller --namespace=kube-system
+$ kubectl create clusterrolebinding tiller-admin --serviceaccount=kube-system:tiller --clusterrole=cluster-admin
+$ helm init --service-account=tiller
+$ helm repo update
+$ helm install stable/nginx-ingress
+$ kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.6/deploy/manifests/00-crds.yaml
+$ helm install --name cert-manager --namespace cert-manager stable/cert-manager
+```
+
+## Create Letsencrypt Issuers and Ingress Services
+
+Copy the configuration templates and change the file according to your needs.
+
+```bash
+# in folder deployment/digital-ocean/https/
+cp templates/issuer.template.yaml ./issuer.yaml
+cp templates/ingress.template.yaml ./ingress.yaml
+```
+
+At least, **change email addresses** in `issuer.yaml`. For sure you also want
+to _change the domain name_ in `ingress.yaml`.
+
+Once you are done, apply the configuration:
+
+```bash
+# in folder deployment/digital-ocean/https/
+$ kubectl apply -f .
+```
+
+By now, your cluster should have a load balancer assigned with an external IP
+address. On Digital Ocean, this is how it should look like:
+
+
+
+Check the ingress server is working correctly:
+
+```bash
+$ curl -kivL -H 'Host: ' 'https://'
+```
+
+If the response looks good, configure your domain registrar for the new IP address and the domain.
+
+Now let's get a valid HTTPS certificate. According to the tutorial above, check your tls certificate for staging:
+
+```bash
+$ kubectl describe --namespace=human-connection certificate tls
+$ kubectl describe --namespace=human-connection secret tls
+```
+
+If everything looks good, update the issuer of your ingress. Change the annotation `certmanager.k8s.io/issuer` from `letsencrypt-staging` to `letsencrypt-prod` in your ingress configuration in `ingress.yaml`.
+
+```bash
+# in folder deployment/digital-ocean/https/
+$ kubectl apply -f ingress.yaml
+```
+
+Delete the former secret to force a refresh:
+
+```text
+$ kubectl --namespace=human-connection delete secret tls
+```
+
+Now, HTTPS should be configured on your domain. Congrats.
diff --git a/Human-Connection/deployment/digital-ocean/https/ip-address.png b/Human-Connection/deployment/digital-ocean/https/ip-address.png
new file mode 100644
index 000000000..db523156a
Binary files /dev/null and b/Human-Connection/deployment/digital-ocean/https/ip-address.png differ
diff --git a/Human-Connection/deployment/digital-ocean/https/namespace.yaml b/Human-Connection/deployment/digital-ocean/https/namespace.yaml
new file mode 100644
index 000000000..0710da55b
--- /dev/null
+++ b/Human-Connection/deployment/digital-ocean/https/namespace.yaml
@@ -0,0 +1,6 @@
+kind: Namespace
+apiVersion: v1
+metadata:
+ name: human-connection
+ labels:
+ name: human-connection
diff --git a/Human-Connection/deployment/digital-ocean/https/templates/ingress.template.yaml b/Human-Connection/deployment/digital-ocean/https/templates/ingress.template.yaml
new file mode 100644
index 000000000..9d0068e08
--- /dev/null
+++ b/Human-Connection/deployment/digital-ocean/https/templates/ingress.template.yaml
@@ -0,0 +1,30 @@
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+ name: ingress
+ namespace: human-connection
+ annotations:
+ kubernetes.io/ingress.class: "nginx"
+ certmanager.k8s.io/issuer: "letsencrypt-staging"
+ certmanager.k8s.io/acme-challenge-type: http01
+spec:
+ tls:
+ - hosts:
+ # - nitro-mailserver.human-connection.org
+ - nitro-staging.human-connection.org
+ secretName: tls
+ rules:
+ - host: nitro-staging.human-connection.org
+ http:
+ paths:
+ - path: /
+ backend:
+ serviceName: nitro-web
+ servicePort: 3000
+ # - host: nitro-mailserver.human-connection.org
+ # http:
+ # paths:
+ # - path: /
+ # backend:
+ # serviceName: mailserver
+ # servicePort: 80
diff --git a/Human-Connection/deployment/digital-ocean/https/templates/issuer.template.yaml b/Human-Connection/deployment/digital-ocean/https/templates/issuer.template.yaml
new file mode 100644
index 000000000..8cb554fc6
--- /dev/null
+++ b/Human-Connection/deployment/digital-ocean/https/templates/issuer.template.yaml
@@ -0,0 +1,34 @@
+---
+ apiVersion: certmanager.k8s.io/v1alpha1
+ kind: Issuer
+ metadata:
+ name: letsencrypt-staging
+ namespace: human-connection
+ spec:
+ acme:
+ # The ACME server URL
+ server: https://acme-staging-v02.api.letsencrypt.org/directory
+ # Email address used for ACME registration
+ email: user@example.com
+ # Name of a secret used to store the ACME account private key
+ privateKeySecretRef:
+ name: letsencrypt-staging
+ # Enable the HTTP-01 challenge provider
+ http01: {}
+---
+ apiVersion: certmanager.k8s.io/v1alpha1
+ kind: Issuer
+ metadata:
+ name: letsencrypt-prod
+ namespace: human-connection
+ spec:
+ acme:
+ # The ACME server URL
+ server: https://acme-v02.api.letsencrypt.org/directory
+ # Email address used for ACME registration
+ email: user@example.com
+ # Name of a secret used to store the ACME account private key
+ privateKeySecretRef:
+ name: letsencrypt-prod
+ # Enable the HTTP-01 challenge provider
+ http01: {}
diff --git a/Human-Connection/deployment/human-connection/README.md b/Human-Connection/deployment/human-connection/README.md
new file mode 100644
index 000000000..8b30e98d6
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/README.md
@@ -0,0 +1,67 @@
+# Kubernetes Configuration for Human Connection
+
+Deploying Human Connection with kubernetes is straight forward. All you have to
+do is to change certain parameters, like domain names and API keys, then you
+just apply our provided configuration files to your cluster.
+
+## Configuration
+
+Change into the `./deployment` directory and copy our provided templates:
+
+```bash
+# in folder deployment/human-connection/
+$ cp templates/secrets.template.yaml ./secrets.yaml
+$ cp templates/configmap.template.yaml ./configmap.yaml
+```
+
+Change the `configmap.yaml` in the `./deployment/human-connection` directory as needed, all variables will be available as
+environment variables in your deployed kubernetes pods.
+
+Probably you want to change this environment variable to your actual domain:
+
+```
+# in configmap.yaml
+CLIENT_URI: "https://nitro-staging.human-connection.org"
+```
+
+If you want to edit secrets, you have to `base64` encode them. See [kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/secret/#creating-a-secret-manually).
+
+```bash
+# example how to base64 a string:
+$ echo -n 'admin' | base64
+YWRtaW4=
+```
+
+Those secrets get `base64` decoded and are available as environment variables in
+your deployed kubernetes pods.
+
+## Create a namespace
+
+```bash
+# in folder deployment/
+$ kubectl apply -f namespace.yaml
+```
+
+If you have a [kubernets dashboard](../digital-ocean/dashboard/README.md)
+deployed you should switch to namespace `human-connection` in order to
+monitor the state of your deployments.
+
+## Create persistent volumes
+
+While the deployments and services can easily be restored, simply by deleting
+and applying the kubernetes configurations again, certain data is not that
+easily recovered. Therefore we separated persistent volumes from deployments
+and services. There is a [dedicated section](../volumes/README.md). Create those
+persistent volumes once before you apply the configuration.
+
+## Apply the configuration
+
+```bash
+# in folder deployment/
+$ kubectl apply -f human-connection/
+```
+
+This can take a while because kubernetes will download the docker images. Sit
+back and relax and have a look into your kubernetes dashboard. Wait until all
+pods turn green and they don't show a warning `Waiting: ContainerCreating`
+anymore.
diff --git a/Human-Connection/deployment/human-connection/deployment-backend.yaml b/Human-Connection/deployment/human-connection/deployment-backend.yaml
new file mode 100644
index 000000000..51f0eb43c
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/deployment-backend.yaml
@@ -0,0 +1,47 @@
+---
+ apiVersion: extensions/v1beta1
+ kind: Deployment
+ metadata:
+ name: nitro-backend
+ namespace: human-connection
+ spec:
+ replicas: 1
+ minReadySeconds: 15
+ progressDeadlineSeconds: 60
+ strategy:
+ rollingUpdate:
+ maxSurge: 0
+ maxUnavailable: "100%"
+ selector:
+ matchLabels:
+ human-connection.org/selector: deployment-human-connection-backend
+ template:
+ metadata:
+ annotations:
+ backup.velero.io/backup-volumes: uploads
+ labels:
+ human-connection.org/commit: COMMIT
+ human-connection.org/selector: deployment-human-connection-backend
+ name: "nitro-backend"
+ spec:
+ containers:
+ - name: nitro-backend
+ image: humanconnection/nitro-backend:latest
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 4000
+ envFrom:
+ - configMapRef:
+ name: configmap
+ - secretRef:
+ name: human-connection
+ volumeMounts:
+ - mountPath: /nitro-backend/public/uploads
+ name: uploads
+ volumes:
+ - name: uploads
+ persistentVolumeClaim:
+ claimName: uploads-claim
+ restartPolicy: Always
+ terminationGracePeriodSeconds: 30
+ status: {}
diff --git a/Human-Connection/deployment/human-connection/deployment-neo4j.yaml b/Human-Connection/deployment/human-connection/deployment-neo4j.yaml
new file mode 100644
index 000000000..3c4887194
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/deployment-neo4j.yaml
@@ -0,0 +1,61 @@
+---
+ apiVersion: extensions/v1beta1
+ kind: Deployment
+ metadata:
+ name: nitro-neo4j
+ namespace: human-connection
+ spec:
+ replicas: 1
+ strategy:
+ rollingUpdate:
+ maxSurge: 0
+ maxUnavailable: "100%"
+ selector:
+ matchLabels:
+ human-connection.org/selector: deployment-human-connection-neo4j
+ template:
+ metadata:
+ annotations:
+ backup.velero.io/backup-volumes: neo4j-data
+ labels:
+ human-connection.org/selector: deployment-human-connection-neo4j
+ name: nitro-neo4j
+ spec:
+ containers:
+ - name: nitro-neo4j
+ image: humanconnection/neo4j:latest
+ imagePullPolicy: Always
+ env:
+ - name: NEO4J_apoc_import_file_enabled
+ value: "true"
+ - name: NEO4J_dbms_memory_pagecache_size
+ value: 1G
+ - name: NEO4J_dbms_memory_heap_max__size
+ value: 1G
+ - name: NEO4J_URI
+ valueFrom:
+ configMapKeyRef:
+ name: configmap
+ key: NEO4J_URI
+ - name: NEO4J_USER
+ valueFrom:
+ configMapKeyRef:
+ name: configmap
+ key: NEO4J_USER
+ - name: NEO4J_AUTH
+ valueFrom:
+ configMapKeyRef:
+ name: configmap
+ key: NEO4J_AUTH
+ ports:
+ - containerPort: 7687
+ - containerPort: 7474
+ volumeMounts:
+ - mountPath: /data/
+ name: neo4j-data
+ volumes:
+ - name: neo4j-data
+ persistentVolumeClaim:
+ claimName: neo4j-data-claim
+ restartPolicy: Always
+ terminationGracePeriodSeconds: 30
diff --git a/Human-Connection/deployment/human-connection/deployment-web.yaml b/Human-Connection/deployment/human-connection/deployment-web.yaml
new file mode 100644
index 000000000..885762e0a
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/deployment-web.yaml
@@ -0,0 +1,37 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: nitro-web
+ namespace: human-connection
+spec:
+ replicas: 2
+ minReadySeconds: 15
+ progressDeadlineSeconds: 60
+ selector:
+ matchLabels:
+ human-connection.org/selector: deployment-human-connection-web
+ template:
+ metadata:
+ labels:
+ human-connection.org/commit: COMMIT
+ human-connection.org/selector: deployment-human-connection-web
+ name: nitro-web
+ spec:
+ containers:
+ - name: web
+ envFrom:
+ - configMapRef:
+ name: configmap
+ - secretRef:
+ name: human-connection
+ env:
+ - name: HOST
+ value: 0.0.0.0
+ image: humanconnection/nitro-web:latest
+ ports:
+ - containerPort: 3000
+ resources: {}
+ imagePullPolicy: Always
+ restartPolicy: Always
+ terminationGracePeriodSeconds: 30
+status: {}
diff --git a/Human-Connection/deployment/human-connection/mailserver/README.md b/Human-Connection/deployment/human-connection/mailserver/README.md
new file mode 100644
index 000000000..9a224a3b9
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/mailserver/README.md
@@ -0,0 +1,18 @@
+# Development Mail Server
+
+You can deploy a fake smtp server which captures all send mails and displays
+them in a web interface. The [sample configuration](../templates/configmap.template.yml)
+is assuming such a dummy server in the `SMTP_HOST` configuration and points to
+a cluster-internal SMTP server.
+
+To deploy the SMTP server just uncomment the relevant code in the
+[ingress server configuration](../../https/templates/ingress.template.yaml) and
+run the following:
+
+```bash
+# in folder deployment/human-connection
+kubectl apply -f mailserver/
+```
+
+You might need to refresh the TLS secret to enable HTTPS on the publicly
+available web interface.
diff --git a/Human-Connection/deployment/human-connection/mailserver/deployment-mailserver.yaml b/Human-Connection/deployment/human-connection/mailserver/deployment-mailserver.yaml
new file mode 100644
index 000000000..d97a66bc9
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/mailserver/deployment-mailserver.yaml
@@ -0,0 +1,34 @@
+---
+ apiVersion: extensions/v1beta1
+ kind: Deployment
+ metadata:
+ name: mailserver
+ namespace: human-connection
+ spec:
+ replicas: 1
+ minReadySeconds: 15
+ progressDeadlineSeconds: 60
+ selector:
+ matchLabels:
+ human-connection.org/selector: deployment-human-connection-mailserver
+ template:
+ metadata:
+ labels:
+ human-connection.org/selector: deployment-human-connection-mailserver
+ name: "mailserver"
+ spec:
+ containers:
+ - name: mailserver
+ image: djfarrelly/maildev
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 80
+ - containerPort: 25
+ envFrom:
+ - configMapRef:
+ name: configmap
+ - secretRef:
+ name: human-connection
+ restartPolicy: Always
+ terminationGracePeriodSeconds: 30
+ status: {}
diff --git a/Human-Connection/deployment/human-connection/mailserver/service-mailserver.yaml b/Human-Connection/deployment/human-connection/mailserver/service-mailserver.yaml
new file mode 100644
index 000000000..655488b89
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/mailserver/service-mailserver.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: mailserver
+ namespace: human-connection
+ labels:
+ human-connection.org/selector: deployment-human-connection-mailserver
+spec:
+ ports:
+ - name: web
+ port: 80
+ targetPort: 80
+ - name: smtp
+ port: 25
+ targetPort: 25
+ selector:
+ human-connection.org/selector: deployment-human-connection-mailserver
diff --git a/Human-Connection/deployment/human-connection/service-backend.yaml b/Human-Connection/deployment/human-connection/service-backend.yaml
new file mode 100644
index 000000000..52e4621b2
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/service-backend.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: nitro-backend
+ namespace: human-connection
+ labels:
+ human-connection.org/selector: deployment-human-connection-backend
+spec:
+ ports:
+ - name: web
+ port: 4000
+ targetPort: 4000
+ selector:
+ human-connection.org/selector: deployment-human-connection-backend
diff --git a/Human-Connection/deployment/human-connection/service-neo4j.yaml b/Human-Connection/deployment/human-connection/service-neo4j.yaml
new file mode 100644
index 000000000..ebe7c5208
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/service-neo4j.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: nitro-neo4j
+ namespace: human-connection
+ labels:
+ human-connection.org/selector: deployment-human-connection-neo4j
+spec:
+ ports:
+ - name: bolt
+ port: 7687
+ targetPort: 7687
+ - name: web
+ port: 7474
+ targetPort: 7474
+ selector:
+ human-connection.org/selector: deployment-human-connection-neo4j
diff --git a/Human-Connection/deployment/human-connection/service-web.yaml b/Human-Connection/deployment/human-connection/service-web.yaml
new file mode 100644
index 000000000..548b874c2
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/service-web.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: nitro-web
+ namespace: human-connection
+ labels:
+ human-connection.org/selector: deployment-human-connection-web
+spec:
+ ports:
+ - name: web
+ port: 3000
+ targetPort: 3000
+ selector:
+ human-connection.org/selector: deployment-human-connection-web
diff --git a/Human-Connection/deployment/human-connection/templates/configmap.template.yaml b/Human-Connection/deployment/human-connection/templates/configmap.template.yaml
new file mode 100644
index 000000000..762901ae8
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/templates/configmap.template.yaml
@@ -0,0 +1,18 @@
+---
+ apiVersion: v1
+ kind: ConfigMap
+ data:
+ SMTP_HOST: "mailserver.human-connection"
+ SMTP_PORT: "25"
+ SMTP_USERNAME: ""
+ SMTP_PASSWORD: ""
+ GRAPHQL_PORT: "4000"
+ GRAPHQL_URI: "http://nitro-backend.human-connection:4000"
+ MOCKS: "false"
+ NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
+ NEO4J_USER: "neo4j"
+ NEO4J_AUTH: "none"
+ CLIENT_URI: "https://nitro-staging.human-connection.org"
+ metadata:
+ name: configmap
+ namespace: human-connection
diff --git a/Human-Connection/deployment/human-connection/templates/secrets.template.yaml b/Human-Connection/deployment/human-connection/templates/secrets.template.yaml
new file mode 100644
index 000000000..9f59b948a
--- /dev/null
+++ b/Human-Connection/deployment/human-connection/templates/secrets.template.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Secret
+data:
+ JWT_SECRET: "Yi8mJjdiNzhCRiZmdi9WZA=="
+ MONGODB_PASSWORD: "TU9OR09EQl9QQVNTV09SRA=="
+ PRIVATE_KEY_PASSPHRASE: "YTdkc2Y3OHNhZGc4N2FkODdzZmFnc2FkZzc4"
+ MAPBOX_TOKEN: "cGsuZXlKMUlqb2lhSFZ0WVc0dFkyOXVibVZqZEdsdmJpSXNJbUVpT2lKamFqbDBjbkJ1Ykdvd2VUVmxNM1Z3WjJsek5UTnVkM1p0SW4wLktaOEtLOWw3MG9talhiRWtrYkhHc1EK"
+ SMTP_HOST:
+ SMTP_PORT: 587
+ SMTP_USERNAME:
+ SMTP_PASSWORD:
+ SMTP_IGNORE_TLS:
+metadata:
+ name: human-connection
+ namespace: human-connection
diff --git a/Human-Connection/deployment/legacy-migration/README.md b/Human-Connection/deployment/legacy-migration/README.md
new file mode 100644
index 000000000..7e8b6a205
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/README.md
@@ -0,0 +1,85 @@
+# Legacy data migration
+
+This setup is **completely optional** and only required if you have data on a
+server which is running our legacy code and you want to import that data. It
+will import the uploads folder and migrate a dump of the legacy Mongo database
+into our new Neo4J graph database.
+
+## Configure Maintenance-Worker Pod
+
+Create a configmap with the specific connection data of your legacy server:
+
+```bash
+$ kubectl create configmap maintenance-worker \
+ --namespace=human-connection \
+ --from-literal=SSH_USERNAME=someuser \
+ --from-literal=SSH_HOST=yourhost \
+ --from-literal=MONGODB_USERNAME=hc-api \
+ --from-literal=MONGODB_PASSWORD=secretpassword \
+ --from-literal=MONGODB_AUTH_DB=hc_api \
+ --from-literal=MONGODB_DATABASE=hc_api \
+ --from-literal=UPLOADS_DIRECTORY=/var/www/api/uploads
+```
+
+Create a secret with your public and private ssh keys. As the [kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/secret/#use-case-pod-with-ssh-keys) points out, you should be careful with your ssh keys. Anyone with access to your cluster will have access to your ssh keys. Better create a new pair with `ssh-keygen` and copy the public key to your legacy server with `ssh-copy-id`:
+
+```bash
+$ kubectl create secret generic ssh-keys \
+ --namespace=human-connection \
+ --from-file=id_rsa=/path/to/.ssh/id_rsa \
+ --from-file=id_rsa.pub=/path/to/.ssh/id_rsa.pub \
+ --from-file=known_hosts=/path/to/.ssh/known_hosts
+```
+
+## Deploy a Temporary Maintenance-Worker Pod
+
+Bring the application into maintenance mode.
+
+{% hint style="info" %} TODO: implement maintenance mode {% endhint %}
+
+
+Then temporarily delete backend and database deployments
+
+```bash
+$ kubectl --namespace=human-connection get deployments
+NAME READY UP-TO-DATE AVAILABLE AGE
+nitro-backend 1/1 1 1 3d11h
+nitro-neo4j 1/1 1 1 3d11h
+nitro-web 2/2 2 2 73d
+$ kubectl --namespace=human-connection delete deployment nitro-neo4j
+deployment.extensions "nitro-neo4j" deleted
+$ kubectl --namespace=human-connection delete deployment nitro-backend
+deployment.extensions "nitro-backend" deleted
+```
+
+Deploy one-time maintenance-worker pod:
+
+```bash
+# in deployment/legacy-migration/
+$ kubectl apply -f maintenance-worker.yaml
+pod/nitro-maintenance-worker created
+```
+
+Import legacy database and uploads:
+
+```bash
+$ kubectl --namespace=human-connection exec -it nitro-maintenance-worker bash
+$ import_legacy_db
+$ import_legacy_uploads
+$ exit
+```
+
+Delete the pod when you're done:
+
+```bash
+$ kubectl --namespace=human-connection delete pod nitro-maintenance-worker
+```
+
+Oh, and of course you have to get those deleted deployments back. One way of
+doing it would be:
+
+```bash
+# in folder deployment/
+$ kubectl apply -f human-connection/deployment-backend.yaml -f human-connection/deployment-neo4j.yaml
+```
+
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker.yaml b/Human-Connection/deployment/legacy-migration/maintenance-worker.yaml
new file mode 100644
index 000000000..a0f354fc9
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker.yaml
@@ -0,0 +1,37 @@
+---
+ kind: Pod
+ apiVersion: v1
+ metadata:
+ name: nitro-maintenance-worker
+ namespace: human-connection
+ spec:
+ containers:
+ - name: nitro-maintenance-worker
+ image: humanconnection/maintenance-worker:latest
+ env:
+ - name: NEO4J_apoc_import_file_enabled
+ value: "true"
+ envFrom:
+ - configMapRef:
+ name: maintenance-worker
+ - configMapRef:
+ name: configmap
+ volumeMounts:
+ - name: secret-volume
+ readOnly: false
+ mountPath: /root/.ssh
+ - name: uploads
+ mountPath: /uploads
+ - name: neo4j-data
+ mountPath: /data/
+ volumes:
+ - name: secret-volume
+ secret:
+ secretName: ssh-keys
+ defaultMode: 0400
+ - name: uploads
+ persistentVolumeClaim:
+ claimName: uploads-claim
+ - name: neo4j-data
+ persistentVolumeClaim:
+ claimName: neo4j-data-claim
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/.dockerignore b/Human-Connection/deployment/legacy-migration/maintenance-worker/.dockerignore
new file mode 100644
index 000000000..59ba63a8b
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/.dockerignore
@@ -0,0 +1 @@
+.ssh/
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/.gitignore b/Human-Connection/deployment/legacy-migration/maintenance-worker/.gitignore
new file mode 100644
index 000000000..59ba63a8b
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/.gitignore
@@ -0,0 +1 @@
+.ssh/
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/Dockerfile b/Human-Connection/deployment/legacy-migration/maintenance-worker/Dockerfile
new file mode 100644
index 000000000..4502d8d69
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/Dockerfile
@@ -0,0 +1,21 @@
+FROM humanconnection/neo4j:latest
+
+ENV NODE_ENV=maintenance
+EXPOSE 7687 7474
+
+ENV BUILD_DEPS="gettext" \
+ RUNTIME_DEPS="libintl"
+
+RUN set -x && \
+ apk add --update $RUNTIME_DEPS && \
+ apk add --virtual build_deps $BUILD_DEPS && \
+ cp /usr/bin/envsubst /usr/local/bin/envsubst && \
+ apk del build_deps
+
+
+RUN apk upgrade --update
+RUN apk add --no-cache mongodb-tools openssh nodejs yarn rsync
+
+COPY known_hosts /root/.ssh/known_hosts
+COPY migration /migration
+COPY ./binaries/* /usr/local/bin/
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/.env b/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/.env
new file mode 100644
index 000000000..773918095
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/.env
@@ -0,0 +1,6 @@
+# SSH Access
+# SSH_USERNAME='username'
+# SSH_HOST='example.org'
+
+# UPLOADS_DIRECTORY=/var/www/api/uploads
+OUTPUT_DIRECTORY='/uploads/'
\ No newline at end of file
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/idle b/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/idle
new file mode 100755
index 000000000..f5b1b2454
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/idle
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+tail -f /dev/null
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_db b/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_db
new file mode 100755
index 000000000..6ffdf8e3f
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_db
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -e
+for var in "SSH_USERNAME" "SSH_HOST" "MONGODB_USERNAME" "MONGODB_PASSWORD" "MONGODB_DATABASE" "MONGODB_AUTH_DB"
+do
+ if [[ -z "${!var}" ]]; then
+ echo "${var} is undefined"
+ exit 1
+ fi
+done
+
+/migration/mongo/export.sh
+/migration/neo4j/import.sh
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_uploads b/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_uploads
new file mode 100755
index 000000000..5c0b67d74
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_uploads
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -e
+
+# import .env config
+set -o allexport
+source $(dirname "$0")/.env
+set +o allexport
+
+for var in "SSH_USERNAME" "SSH_HOST" "UPLOADS_DIRECTORY"
+do
+ if [[ -z "${!var}" ]]; then
+ echo "${var} is undefined"
+ exit 1
+ fi
+done
+
+rsync --archive --update --verbose ${SSH_USERNAME}@${SSH_HOST}:${UPLOADS_DIRECTORY}/ ${OUTPUT_DIRECTORY}
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/known_hosts b/Human-Connection/deployment/legacy-migration/maintenance-worker/known_hosts
new file mode 100644
index 000000000..947840cb2
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/known_hosts
@@ -0,0 +1,3 @@
+|1|GuOYlVEhTowidPs18zj9p5F2j3o=|sDHJYLz9Ftv11oXeGEjs7SpVyg0= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBM5N29bI5CeKu1/RBPyM2fwyf7fuajOO+tyhKe1+CC2sZ1XNB5Ff6t6MtCLNRv2mUuvzTbW/HkisDiA5tuXUHOk=
+|1|2KP9NV+Q5g2MrtjAeFSVcs8YeOI=|nf3h4wWVwC4xbBS1kzgzE2tBldk= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNhRK6BeIEUxXlS0z/pOfkUkSPfn33g4J1U3L+MyUQYHm+7agT08799ANJhnvELKE1tt4Vx80I9UR81oxzZcy3E=
+|1|HonYIRNhKyroUHPKU1HSZw0+Qzs=|5T1btfwFBz2vNSldhqAIfTbfIgQ= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNhRK6BeIEUxXlS0z/pOfkUkSPfn33g4J1U3L+MyUQYHm+7agT08799ANJhnvELKE1tt4Vx80I9UR81oxzZcy3E=
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/mongo/.env b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/mongo/.env
new file mode 100644
index 000000000..4c5f9e18c
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/mongo/.env
@@ -0,0 +1,17 @@
+# SSH Access
+# SSH_USERNAME='username'
+# SSH_HOST='example.org'
+
+# Mongo DB on Remote Maschine
+# MONGODB_USERNAME='mongouser'
+# MONGODB_PASSWORD='mongopassword'
+# MONGODB_DATABASE='mongodatabase'
+# MONGODB_AUTH_DB='admin'
+
+# Export Settings
+# On Windows this resolves to C:\Users\dornhoeschen\AppData\Local\Temp\mongo-export (MinGW)
+EXPORT_PATH='/tmp/mongo-export/'
+EXPORT_MONGOEXPORT_BIN='mongoexport'
+MONGO_EXPORT_SPLIT_SIZE=100
+# On Windows use something like this
+# EXPORT_MONGOEXPORT_BIN='C:\Program Files\MongoDB\Server\3.6\bin\mongoexport.exe'
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh
new file mode 100755
index 000000000..8d16f42fa
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+set -e
+
+# import .env config
+set -o allexport
+source $(dirname "$0")/.env
+set +o allexport
+
+# Export collection function defintion
+function export_collection () {
+ "${EXPORT_MONGOEXPORT_BIN}" --db ${MONGODB_DATABASE} --host localhost -d ${MONGODB_DATABASE} --port 27018 --username ${MONGODB_USERNAME} --password ${MONGODB_PASSWORD} --authenticationDatabase ${MONGODB_AUTH_DB} --collection $1 --out "${EXPORT_PATH}$1.json"
+ mkdir -p ${EXPORT_PATH}splits/$1/
+ split -l ${MONGO_EXPORT_SPLIT_SIZE} -a 3 ${EXPORT_PATH}$1.json ${EXPORT_PATH}splits/$1/
+}
+
+# Export collection with query function defintion
+function export_collection_query () {
+ "${EXPORT_MONGOEXPORT_BIN}" --db ${MONGODB_DATABASE} --host localhost -d ${MONGODB_DATABASE} --port 27018 --username ${MONGODB_USERNAME} --password ${MONGODB_PASSWORD} --authenticationDatabase ${MONGODB_AUTH_DB} --collection $1 --out "${EXPORT_PATH}$1_$3.json" --query "$2"
+ mkdir -p ${EXPORT_PATH}splits/$1_$3/
+ split -l ${MONGO_EXPORT_SPLIT_SIZE} -a 3 ${EXPORT_PATH}$1_$3.json ${EXPORT_PATH}splits/$1_$3/
+}
+
+# Delete old export & ensure directory
+rm -rf ${EXPORT_PATH}*
+mkdir -p ${EXPORT_PATH}
+
+# Open SSH Tunnel
+ssh -4 -M -S my-ctrl-socket -fnNT -L 27018:localhost:27017 -l ${SSH_USERNAME} ${SSH_HOST}
+
+# Export all Data from the Alpha to json and split them up
+export_collection "badges"
+export_collection "categories"
+export_collection "comments"
+export_collection_query "contributions" "{'type': 'DELETED'}" "DELETED"
+export_collection_query "contributions" "{'type': 'post'}" "post"
+export_collection_query "contributions" "{'type': 'cando'}" "cando"
+export_collection "emotions"
+export_collection_query "follows" "{'foreignService': 'organizations'}" "organizations"
+export_collection_query "follows" "{'foreignService': 'users'}" "users"
+export_collection "invites"
+export_collection "notifications"
+export_collection "organizations"
+export_collection "pages"
+export_collection "projects"
+export_collection "settings"
+export_collection "shouts"
+export_collection "status"
+export_collection "systemnotifications"
+export_collection "users"
+export_collection "userscandos"
+export_collection "usersettings"
+
+# Close SSH Tunnel
+ssh -S my-ctrl-socket -O check -l ${SSH_USERNAME} ${SSH_HOST}
+ssh -S my-ctrl-socket -O exit -l ${SSH_USERNAME} ${SSH_HOST}
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/.env b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/.env
new file mode 100644
index 000000000..16220f3e6
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/.env
@@ -0,0 +1,16 @@
+# Neo4J Settings
+# NEO4J_USERNAME='neo4j'
+# NEO4J_PASSWORD='letmein'
+
+# Import Settings
+# On Windows this resolves to C:\Users\dornhoeschen\AppData\Local\Temp\mongo-export (MinGW)
+IMPORT_PATH='/tmp/mongo-export/'
+IMPORT_CHUNK_PATH='/tmp/mongo-export/splits/'
+
+IMPORT_CHUNK_PATH_CQL='/tmp/mongo-export/splits/'
+# On Windows this path needs to be windows style since the cypher-shell runs native - note the forward slash
+# IMPORT_CHUNK_PATH_CQL='C:/Users/dornhoeschen/AppData/Local/Temp/mongo-export/splits/'
+
+IMPORT_CYPHERSHELL_BIN='cypher-shell'
+# On Windows use something like this
+# IMPORT_CYPHERSHELL_BIN='C:\Program Files\neo4j-community\bin\cypher-shell.bat'
\ No newline at end of file
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql
new file mode 100644
index 000000000..027cea019
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql
@@ -0,0 +1,52 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[?] image: {
+[?] path: { // Path is incorrect in Nitro - is icon the correct name for this field?
+[X] type: String,
+[X] required: true
+ },
+[ ] alt: { // If we use an image - should we not have an alt?
+[ ] type: String,
+[ ] required: true
+ }
+ },
+[?] status: {
+[X] type: String,
+[X] enum: ['permanent', 'temporary'],
+[ ] default: 'permanent', // Default value is missing in Nitro
+[X] required: true
+ },
+[?] type: {
+[?] type: String, // in nitro this is a defined enum - seems good for now
+[X] required: true
+ },
+[X] key: {
+[X] type: String,
+[X] required: true
+ },
+[?] createdAt: {
+[?] type: Date, // Type is modeled as string in Nitro which is incorrect
+[ ] default: Date.now // Default value is missing in Nitro
+ },
+[?] updatedAt: {
+[?] type: Date, // Type is modeled as string in Nitro which is incorrect
+[ ] default: Date.now // Default value is missing in Nitro
+ }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as badge
+MERGE(b:Badge {id: badge._id["$oid"]})
+ON CREATE SET
+b.key = badge.key,
+b.type = badge.type,
+b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''),
+b.status = badge.status,
+b.createdAt = badge.createdAt.`$date`,
+b.updatedAt = badge.updatedAt.`$date`
+;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/delete.cql
new file mode 100644
index 000000000..2a6f8c244
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/delete.cql
@@ -0,0 +1 @@
+MATCH (n:Badge) DETACH DELETE n;
\ No newline at end of file
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories/categories.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories/categories.cql
new file mode 100644
index 000000000..0862fe0d9
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories/categories.cql
@@ -0,0 +1,121 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[X] title: {
+[X] type: String,
+[X] required: true
+ },
+[?] slug: {
+[X] type: String,
+[ ] required: true, // Not required in Nitro
+[ ] unique: true // Unique value is not enforced in Nitro?
+ },
+[?] icon: { // Nitro adds required: true
+[X] type: String,
+[ ] unique: true // Unique value is not enforced in Nitro?
+ },
+[?] createdAt: {
+[?] type: Date, // Type is modeled as string in Nitro which is incorrect
+[ ] default: Date.now // Default value is missing in Nitro
+ },
+[?] updatedAt: {
+[?] type: Date, // Type is modeled as string in Nitro which is incorrect
+[ ] default: Date.now // Default value is missing in Nitro
+ }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as category
+MERGE(c:Category {id: category._id["$oid"]})
+ON CREATE SET
+c.name = category.title,
+c.slug = category.slug,
+c.icon = category.icon,
+c.createdAt = category.createdAt.`$date`,
+c.updatedAt = category.updatedAt.`$date`
+;
+
+// Transform icon names
+MATCH (c:Category)
+WHERE (c.icon = "categories-justforfun")
+SET c.icon = 'smile'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-luck")
+SET c.icon = 'heart-o'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-health")
+SET c.icon = 'medkit'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-environment")
+SET c.icon = 'tree'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-animal-justice")
+SET c.icon = 'paw'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-human-rights")
+SET c.icon = 'balance-scale'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-education")
+SET c.icon = 'graduation-cap'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-cooperation")
+SET c.icon = 'users'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-politics")
+SET c.icon = 'university'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-economy")
+SET c.icon = 'money'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-technology")
+SET c.icon = 'flash'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-internet")
+SET c.icon = 'mouse-pointer'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-art")
+SET c.icon = 'paint-brush'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-freedom-of-speech")
+SET c.icon = 'bullhorn'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-sustainability")
+SET c.icon = 'shopping-cart'
+;
+
+MATCH (c:Category)
+WHERE (c.icon = "categories-peace")
+SET c.icon = 'angellist'
+;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories/delete.cql
new file mode 100644
index 000000000..c06b5ef2b
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories/delete.cql
@@ -0,0 +1 @@
+MATCH (n:Category) DETACH DELETE n;
\ No newline at end of file
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments/comments.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments/comments.cql
new file mode 100644
index 000000000..1cdc1bfc2
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments/comments.cql
@@ -0,0 +1,65 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[?] userId: {
+[X] type: String,
+[ ] required: true, // Not required in Nitro
+[-] index: true
+ },
+[?] contributionId: {
+[X] type: String,
+[ ] required: true, // Not required in Nitro
+[-] index: true
+ },
+[X] content: {
+[X] type: String,
+[X] required: true
+ },
+[?] contentExcerpt: { // Generated from content
+[X] type: String,
+[ ] required: true // Not required in Nitro
+ },
+[ ] hasMore: { type: Boolean },
+[ ] upvotes: {
+[ ] type: Array,
+[ ] default: []
+ },
+[ ] upvoteCount: {
+[ ] type: Number,
+[ ] default: 0
+ },
+[?] deleted: {
+[X] type: Boolean,
+[ ] default: false, // Default value is missing in Nitro
+[-] index: true
+ },
+[ ] createdAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] updatedAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as comment
+MERGE (c:Comment {id: comment._id["$oid"]})
+ON CREATE SET
+c.content = comment.content,
+c.contentExcerpt = comment.contentExcerpt,
+c.deleted = comment.deleted,
+c.disabled = false
+WITH c, comment, comment.contributionId as postId
+MATCH (post:Post {id: postId})
+WITH c, post, comment.userId as userId
+MATCH (author:User {id: userId})
+MERGE (c)-[:COMMENTS]->(post)
+MERGE (author)-[:WROTE]->(c)
+;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments/delete.cql
new file mode 100644
index 000000000..c4a7961c5
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments/delete.cql
@@ -0,0 +1 @@
+MATCH (n:Comment) DETACH DELETE n;
\ No newline at end of file
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql
new file mode 100644
index 000000000..a0f1418aa
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql
@@ -0,0 +1,153 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+[?] { //Modeled incorrect as Post
+[?] userId: {
+[X] type: String,
+[ ] required: true, // Not required in Nitro
+[-] index: true
+ },
+[ ] organizationId: {
+[ ] type: String,
+[-] index: true
+ },
+[X] categoryIds: {
+[X] type: Array,
+[-] index: true
+ },
+[X] title: {
+[X] type: String,
+[X] required: true
+ },
+[?] slug: { // Generated from title
+[X] type: String,
+[ ] required: true, // Not required in Nitro
+[?] unique: true, // Unique value is not enforced in Nitro?
+[-] index: true
+ },
+[ ] type: { // db.getCollection('contributions').distinct('type') -> 'DELETED', 'cando', 'post'
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] cando: {
+[ ] difficulty: {
+[ ] type: String,
+[ ] enum: ['easy', 'medium', 'hard']
+ },
+[ ] reasonTitle: { type: String },
+[ ] reason: { type: String }
+ },
+[X] content: {
+[X] type: String,
+[X] required: true
+ },
+[?] contentExcerpt: { // Generated from content
+[X] type: String,
+[?] required: true // Not required in Nitro
+ },
+[ ] hasMore: { type: Boolean },
+[X] teaserImg: { type: String },
+[ ] language: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] shoutCount: {
+[ ] type: Number,
+[ ] default: 0,
+[-] index: true
+ },
+[ ] meta: {
+[ ] hasVideo: {
+[ ] type: Boolean,
+[ ] default: false
+ },
+[ ] embedds: {
+[ ] type: Object,
+[ ] default: {}
+ }
+ },
+[?] visibility: {
+[X] type: String,
+[X] enum: ['public', 'friends', 'private'],
+[ ] default: 'public', // Default value is missing in Nitro
+[-] index: true
+ },
+[?] isEnabled: {
+[X] type: Boolean,
+[ ] default: true, // Default value is missing in Nitro
+[-] index: true
+ },
+[?] tags: { type: Array }, // ensure this is working properly
+[ ] emotions: {
+[ ] type: Object,
+[-] index: true,
+[ ] default: {
+[ ] angry: {
+[ ] count: 0,
+[ ] percent: 0
+[ ] },
+[ ] cry: {
+[ ] count: 0,
+[ ] percent: 0
+[ ] },
+[ ] surprised: {
+[ ] count: 0,
+[ ] percent: 0
+ },
+[ ] happy: {
+[ ] count: 0,
+[ ] percent: 0
+ },
+[ ] funny: {
+[ ] count: 0,
+[ ] percent: 0
+ }
+ }
+ },
+[?] deleted: { // THis field is not always present in the alpha-data
+[?] type: Boolean,
+[ ] default: false, // Default value is missing in Nitro
+[-] index: true
+ },
+[?] createdAt: {
+[?] type: Date, // Type is modeled as string in Nitro which is incorrect
+[ ] default: Date.now // Default value is missing in Nitro
+ },
+[?] updatedAt: {
+[?] type: Date, // Type is modeled as string in Nitro which is incorrect
+[ ] default: Date.now // Default value is missing in Nitro
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as post
+MERGE (p:Post {id: post._id["$oid"]})
+ON CREATE SET
+p.title = post.title,
+p.slug = post.slug,
+p.image = replace(post.teaserImg, 'https://api-alpha.human-connection.org', ''),
+p.content = post.content,
+p.contentExcerpt = post.contentExcerpt,
+p.visibility = toLower(post.visibility),
+p.createdAt = post.createdAt.`$date`,
+p.updatedAt = post.updatedAt.`$date`,
+p.deleted = COALESCE(post.deleted,false),
+p.disabled = NOT post.isEnabled
+WITH p, post
+MATCH (u:User {id: post.userId})
+MERGE (u)-[:WROTE]->(p)
+WITH p, post, post.categoryIds as categoryIds
+UNWIND categoryIds AS categoryId
+MATCH (c:Category {id: categoryId})
+MERGE (p)-[:CATEGORIZED]->(c)
+WITH p, post.tags AS tags
+UNWIND tags AS tag
+MERGE (t:Tag {id: tag, name: tag})
+MERGE (p)-[:TAGGED]->(t)
+;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/delete.cql
new file mode 100644
index 000000000..70adad664
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/delete.cql
@@ -0,0 +1,2 @@
+MATCH (n:Post) DETACH DELETE n;
+MATCH (n:Tag) DETACH DELETE n;
\ No newline at end of file
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/delete_all.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/delete_all.cql
new file mode 100644
index 000000000..d01871300
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/delete_all.cql
@@ -0,0 +1 @@
+MATCH (n) DETACH DELETE n;
\ No newline at end of file
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql
new file mode 100644
index 000000000..8aad9e923
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql
@@ -0,0 +1,35 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[ ] userId: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] contributionId: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] rated: {
+[ ] type: String,
+[ ] required: true,
+[ ] enum: ['funny', 'happy', 'surprised', 'cry', 'angry']
+ },
+[ ] createdAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] updatedAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as emotion;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql
new file mode 100644
index 000000000..3624448c3
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql
@@ -0,0 +1 @@
+// this is just a relation between users(?) - no need to delete
\ No newline at end of file
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/follows.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/follows.cql
new file mode 100644
index 000000000..fac858a9a
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/follows.cql
@@ -0,0 +1,36 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[?] userId: {
+[-] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[?] foreignId: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[?] foreignService: { // db.getCollection('follows').distinct('foreignService') returns 'organizations' and 'users'
+[ ] type: String,
+[ ] required: true,
+[ ] index: true
+ },
+[ ] createdAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+ index:
+[?] { userId: 1, foreignId: 1, foreignService: 1 },{ unique: true } // is the unique constrain modeled?
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as follow
+MATCH (u1:User {id: follow.userId}), (u2:User {id: follow.foreignId})
+MERGE (u1)-[:FOLLOWS]->(u2)
+;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh
new file mode 100755
index 000000000..8eef68c92
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh
@@ -0,0 +1,108 @@
+#!/usr/bin/env bash
+set -e
+
+# import .env config
+set -o allexport
+source $(dirname "$0")/.env
+set +o allexport
+
+# Delete collection function defintion
+function delete_collection () {
+ # Delete from Database
+ echo "Delete $2"
+ "${IMPORT_CYPHERSHELL_BIN}" < $(dirname "$0")/$1/delete.cql > /dev/null
+ # Delete index file
+ rm -f "${IMPORT_PATH}splits/$2.index"
+}
+
+# Import collection function defintion
+function import_collection () {
+ # index file of those chunks we have already imported
+ INDEX_FILE="${IMPORT_PATH}splits/$1.index"
+ # load index file
+ if [ -f "$INDEX_FILE" ]; then
+ readarray -t IMPORT_INDEX <$INDEX_FILE
+ else
+ declare -a IMPORT_INDEX
+ fi
+ # for each chunk import data
+ for chunk in ${IMPORT_PATH}splits/$1/*
+ do
+ CHUNK_FILE_NAME=$(basename "${chunk}")
+ # does the index not contain the chunk file name?
+ if [[ ! " ${IMPORT_INDEX[@]} " =~ " ${CHUNK_FILE_NAME} " ]]; then
+ # calculate the path of the chunk
+ export IMPORT_CHUNK_PATH_CQL_FILE="${IMPORT_CHUNK_PATH_CQL}$1/${CHUNK_FILE_NAME}"
+ # load the neo4j command and replace file variable with actual path
+ NEO4J_COMMAND="$(envsubst '${IMPORT_CHUNK_PATH_CQL_FILE}' < $(dirname "$0")/$2)"
+ # run the import of the chunk
+ echo "Import $1 ${CHUNK_FILE_NAME} (${chunk})"
+ echo "${NEO4J_COMMAND}" | "${IMPORT_CYPHERSHELL_BIN}" > /dev/null
+ # add file to array and file
+ IMPORT_INDEX+=("${CHUNK_FILE_NAME}")
+ echo "${CHUNK_FILE_NAME}" >> ${INDEX_FILE}
+ else
+ echo "Skipping $1 ${CHUNK_FILE_NAME} (${chunk})"
+ fi
+ done
+}
+
+# Time variable
+SECONDS=0
+
+# Delete all Neo4J Database content
+echo "Deleting Database Contents"
+delete_collection "badges" "badges"
+delete_collection "categories" "categories"
+delete_collection "users" "users"
+delete_collection "follows" "follows_users"
+delete_collection "contributions" "contributions_post"
+delete_collection "contributions" "contributions_cando"
+delete_collection "shouts" "shouts"
+delete_collection "comments" "comments"
+
+#delete_collection "emotions"
+#delete_collection "invites"
+#delete_collection "notifications"
+#delete_collection "organizations"
+#delete_collection "pages"
+#delete_collection "projects"
+#delete_collection "settings"
+#delete_collection "status"
+#delete_collection "systemnotifications"
+#delete_collection "userscandos"
+#delete_collection "usersettings"
+echo "DONE"
+
+# Import Data
+echo "Start Importing Data"
+import_collection "badges" "badges/badges.cql"
+import_collection "categories" "categories/categories.cql"
+import_collection "users" "users/users.cql"
+import_collection "follows_users" "follows/follows.cql"
+#import_collection "follows_organizations" "follows/follows.cql"
+import_collection "contributions_post" "contributions/contributions.cql"
+import_collection "contributions_cando" "contributions/contributions.cql"
+#import_collection "contributions_DELETED" "contributions/contributions.cql"
+import_collection "shouts" "shouts/shouts.cql"
+import_collection "comments" "comments/comments.cql"
+
+# import_collection "emotions"
+# import_collection "invites"
+# import_collection "notifications"
+# import_collection "organizations"
+# import_collection "pages"
+# import_collection "systemnotifications"
+# import_collection "userscandos"
+# import_collection "usersettings"
+
+# does only contain dummy data
+# import_collection "projects"
+
+# does only contain alpha specifc data
+# import_collection "status
+# import_collection "settings""
+
+echo "DONE"
+
+echo "Time elapsed: $SECONDS seconds"
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites/delete.cql
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites/invites.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites/invites.cql
new file mode 100644
index 000000000..f4a5bf006
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites/invites.cql
@@ -0,0 +1,39 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[ ] email: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true,
+[ ] unique: true
+ },
+[ ] code: {
+[ ] type: String,
+[-] index: true,
+[ ] required: true
+ },
+[ ] role: {
+[ ] type: String,
+[ ] enum: ['admin', 'moderator', 'manager', 'editor', 'user'],
+[ ] default: 'user'
+ },
+[ ] invitedByUserId: { type: String },
+[ ] language: { type: String },
+[ ] badgeIds: [],
+[ ] wasUsed: {
+[ ] type: Boolean,
+[-] index: true
+ },
+[ ] createdAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as invite;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications/delete.cql
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications/notifications.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications/notifications.cql
new file mode 100644
index 000000000..aa6ac8eb9
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications/notifications.cql
@@ -0,0 +1,48 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[ ] userId: { // User this notification is sent to
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] type: {
+[ ] type: String,
+[ ] required: true,
+[ ] enum: ['comment','comment-mention','contribution-mention','following-contribution']
+ },
+[ ] relatedUserId: {
+[ ] type: String,
+[-] index: true
+ },
+[ ] relatedContributionId: {
+[ ] type: String,
+[-] index: true
+ },
+[ ] relatedOrganizationId: {
+[ ] type: String,
+[-] index: true
+ },
+[ ] relatedCommentId: {type: String },
+[ ] unseen: {
+[ ] type: Boolean,
+[ ] default: true,
+[-] index: true
+ },
+[ ] createdAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] updatedAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as notification;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations/delete.cql
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations/organizations.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations/organizations.cql
new file mode 100644
index 000000000..e473e697c
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations/organizations.cql
@@ -0,0 +1,137 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[ ] name: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] slug: {
+[ ] type: String,
+[ ] required: true,
+[ ] unique: true,
+[-] index: true
+ },
+[ ] followersCounts: {
+[ ] users: {
+[ ] type: Number,
+[ ] default: 0
+ },
+[ ] organizations: {
+[ ] type: Number,
+[ ] default: 0
+ },
+[ ] projects: {
+[ ] type: Number,
+[ ] default: 0
+ }
+ },
+[ ] followingCounts: {
+[ ] users: {
+[ ] type: Number,
+[ ] default: 0
+ },
+[ ] organizations: {
+[ ] type: Number,
+[ ] default: 0
+ },
+[ ] projects: {
+[ ] type: Number,
+[ ] default: 0
+ }
+ },
+[ ] categoryIds: {
+[ ] type: Array,
+[ ] required: true,
+[-] index: true
+ },
+[ ] logo: { type: String },
+[ ] coverImg: { type: String },
+[ ] userId: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] description: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] descriptionExcerpt: { type: String }, // will be generated automatically
+[ ] publicEmail: { type: String },
+[ ] url: { type: String },
+[ ] type: {
+[ ] type: String,
+[-] index: true,
+[ ] enum: ['ngo', 'npo', 'goodpurpose', 'ev', 'eva']
+ },
+[ ] language: {
+[ ] type: String,
+[ ] required: true,
+[ ] default: 'de',
+[-] index: true
+ },
+[ ] addresses: {
+[ ] type: [{
+[ ] street: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] zipCode: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] city: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] country: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] lat: {
+[ ] type: Number,
+[ ] required: true
+ },
+[ ] lng: {
+[ ] type: Number,
+[ ] required: true
+ }
+ }],
+[ ] default: []
+ },
+[ ] createdAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] updatedAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] isEnabled: {
+[ ] type: Boolean,
+[ ] default: false,
+[-] index: true
+ },
+[ ] reviewedBy: {
+[ ] type: String,
+[ ] default: null,
+[-] index: true
+ },
+[ ] tags: {
+[ ] type: Array,
+[-] index: true
+ },
+[ ] deleted: {
+[ ] type: Boolean,
+[ ] default: false,
+[-] index: true
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as organisation;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages/delete.cql
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages/pages.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages/pages.cql
new file mode 100644
index 000000000..18223136b
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages/pages.cql
@@ -0,0 +1,55 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[ ] title: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] slug: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] type: {
+[ ] type: String,
+[ ] required: true,
+[ ] default: 'page'
+ },
+[ ] key: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] content: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] language: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] active: {
+[ ] type: Boolean,
+[ ] default: true,
+[-] index: true
+ },
+[ ] createdAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] updatedAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+ index:
+[ ] { slug: 1, language: 1 },{ unique: true }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as page;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects/delete.cql
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects/projects.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects/projects.cql
new file mode 100644
index 000000000..ed859c157
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects/projects.cql
@@ -0,0 +1,44 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[ ] name: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] slug: { type: String },
+[ ] followerIds: [],
+[ ] categoryIds: { type: Array },
+[ ] logo: { type: String },
+[ ] userId: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] description: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] content: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] addresses: {
+[ ] type: Array,
+[ ] default: []
+ },
+[ ] createdAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] updatedAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as project;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings/delete.cql
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings/settings.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings/settings.cql
new file mode 100644
index 000000000..1d557d30c
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings/settings.cql
@@ -0,0 +1,36 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[ ] key: {
+[ ] type: String,
+[ ] default: 'system',
+[-] index: true,
+[ ] unique: true
+ },
+[ ] invites: {
+[ ] userCanInvite: {
+[ ] type: Boolean,
+[ ] required: true,
+[ ] default: false
+ },
+[ ] maxInvitesByUser: {
+[ ] type: Number,
+[ ] required: true,
+[ ] default: 1
+ },
+[ ] onlyUserWithBadgesCanInvite: {
+[ ] type: Array,
+[ ] default: []
+ }
+ },
+[ ] maintenance: false
+ }, {
+[ ] timestamps: true
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as setting;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts/delete.cql
new file mode 100644
index 000000000..21c2e1f90
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts/delete.cql
@@ -0,0 +1 @@
+// this is just a relation between users and contributions - no need to delete
\ No newline at end of file
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts/shouts.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts/shouts.cql
new file mode 100644
index 000000000..d370b4b4a
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts/shouts.cql
@@ -0,0 +1,36 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[?] userId: {
+[X] type: String,
+[ ] required: true, // Not required in Nitro
+[-] index: true
+ },
+[?] foreignId: {
+[X] type: String,
+[ ] required: true, // Not required in Nitro
+[-] index: true
+ },
+[?] foreignService: { // db.getCollection('shots').distinct('foreignService') returns 'contributions'
+[X] type: String,
+[ ] required: true, // Not required in Nitro
+[-] index: true
+ },
+[ ] createdAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+ index:
+[?] { userId: 1, foreignId: 1 },{ unique: true } // is the unique constrain modeled?
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as shout
+MATCH (u:User {id: shout.userId}), (p:Post {id: shout.foreignId})
+MERGE (u)-[:SHOUTED]->(p)
+;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/status/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/status/delete.cql
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/status/status.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/status/status.cql
new file mode 100644
index 000000000..010c2ca09
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/status/status.cql
@@ -0,0 +1,19 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[ ] maintenance: {
+[ ] type: Boolean,
+[ ] default: false
+ },
+[ ] updatedAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as status;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications/delete.cql
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications/systemnotifications.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications/systemnotifications.cql
new file mode 100644
index 000000000..4bd33eb7c
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications/systemnotifications.cql
@@ -0,0 +1,61 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[ ] type: {
+[ ] type: String,
+[ ] default: 'info',
+[ ] required: true,
+[-] index: true
+ },
+[ ] title: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] content: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] slot: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] language: {
+[ ] type: String,
+[ ] required: true,
+[-] index: true
+ },
+[ ] permanent: {
+[ ] type: Boolean,
+[ ] default: false
+ },
+[ ] requireConfirmation: {
+[ ] type: Boolean,
+[ ] default: false
+ },
+[ ] active: {
+[ ] type: Boolean,
+[ ] default: true,
+[-] index: true
+ },
+[ ] totalCount: {
+[ ] type: Number,
+[ ] default: 0
+ },
+[ ] createdAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] updatedAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as systemnotification;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/delete.cql
new file mode 100644
index 000000000..23935b3e0
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/delete.cql
@@ -0,0 +1 @@
+MATCH (n:User) DETACH DELETE n;
\ No newline at end of file
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql
new file mode 100644
index 000000000..4d7c9aa9f
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql
@@ -0,0 +1,118 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[?] email: {
+[X] type: String,
+[-] index: true,
+[X] required: true,
+[?] unique: true //unique constrain missing in Nitro
+ },
+[?] password: { // Not required in Alpha -> verify if always present
+[X] type: String
+ },
+[X] name: { type: String },
+[X] slug: {
+[X] type: String,
+[-] index: true
+ },
+[ ] gender: { type: String },
+[ ] followersCounts: {
+[ ] users: {
+[ ] type: Number,
+[ ] default: 0
+ },
+[ ] organizations: {
+[ ] type: Number,
+[ ] default: 0
+ },
+[ ] projects: {
+[ ] type: Number,
+[ ] default: 0
+ }
+ },
+[ ] followingCounts: {
+[ ] users: {
+[ ] type: Number,
+[ ] default: 0
+ },
+[ ] organizations: {
+[ ] type: Number,
+[ ] default: 0
+ },
+[ ] projects: {
+[ ] type: Number,
+[ ] default: 0
+ }
+ },
+[ ] timezone: { type: String },
+[X] avatar: { type: String },
+[X] coverImg: { type: String },
+[ ] doiToken: { type: String },
+[ ] confirmedAt: { type: Date },
+[?] badgeIds: [], // Verify this is working properly
+[?] deletedAt: { type: Date }, // The Date of deletion is not saved in Nitro
+[?] createdAt: {
+[?] type: Date, // Modeled as String in Nitro
+[ ] default: Date.now // Default value is missing in Nitro
+ },
+[?] updatedAt: {
+[?] type: Date, // Modeled as String in Nitro
+[ ] default: Date.now // Default value is missing in Nitro
+ },
+[ ] lastActiveAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] isVerified: { type: Boolean },
+[?] role: {
+[X] type: String,
+[-] index: true,
+[?] enum: ['admin', 'moderator', 'manager', 'editor', 'user'], // missing roles manager & editor in Nitro
+[ ] default: 'user' // Default value is missing in Nitro
+ },
+[ ] verifyToken: { type: String },
+[ ] verifyShortToken: { type: String },
+[ ] verifyExpires: { type: Date },
+[ ] verifyChanges: { type: Object },
+[ ] resetToken: { type: String },
+[ ] resetShortToken: { type: String },
+[ ] resetExpires: { type: Date },
+[X] wasSeeded: { type: Boolean },
+[X] wasInvited: { type: Boolean },
+[ ] language: {
+[ ] type: String,
+[ ] default: 'en'
+ },
+[ ] termsAndConditionsAccepted: { type: Date }, // we display the terms and conditions on registration
+[ ] systemNotificationsSeen: {
+[ ] type: Array,
+[ ] default: []
+ }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as user
+MERGE(u:User {id: user._id["$oid"]})
+ON CREATE SET
+u.name = user.name,
+u.slug = user.slug,
+u.email = user.email,
+u.password = user.password,
+u.avatar = replace(user.avatar, 'https://api-alpha.human-connection.org', ''),
+u.coverImg = replace(user.coverImg, 'https://api-alpha.human-connection.org', ''),
+u.wasInvited = user.wasInvited,
+u.wasSeeded = user.wasSeeded,
+u.role = toLower(user.role),
+u.createdAt = user.createdAt.`$date`,
+u.updatedAt = user.updatedAt.`$date`,
+u.deleted = user.deletedAt IS NOT NULL,
+u.disabled = false
+WITH u, user, user.badgeIds AS badgeIds
+UNWIND badgeIds AS badgeId
+MATCH (b:Badge {id: badgeId})
+MERGE (b)-[:REWARDED]->(u)
+;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos/delete.cql
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos/userscandos.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos/userscandos.cql
new file mode 100644
index 000000000..55f58f171
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos/userscandos.cql
@@ -0,0 +1,35 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[ ] userId: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] contributionId: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] done: {
+[ ] type: Boolean,
+[ ] default: false
+ },
+[ ] doneAt: { type: Date },
+[ ] createdAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] updatedAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ },
+[ ] wasSeeded: { type: Boolean }
+ }
+ index:
+[ ] { userId: 1, contributionId: 1 },{ unique: true }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as usercando;
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings/delete.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings/delete.cql
new file mode 100644
index 000000000..e69de29bb
diff --git a/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings/usersettings.cql b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings/usersettings.cql
new file mode 100644
index 000000000..722625944
--- /dev/null
+++ b/Human-Connection/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings/usersettings.cql
@@ -0,0 +1,43 @@
+/*
+// Alpha Model
+// [ ] Not modeled in Nitro
+// [X] Modeled in Nitro
+// [-] Omitted in Nitro
+// [?] Unclear / has work to be done for Nitro
+ {
+[ ] userId: {
+[ ] type: String,
+[ ] required: true,
+[ ] unique: true
+ },
+[ ] blacklist: {
+[ ] type: Array,
+[ ] default: []
+ },
+[ ] uiLanguage: {
+[ ] type: String,
+[ ] required: true
+ },
+[ ] contentLanguages: {
+[ ] type: Array,
+[ ] default: []
+ },
+[ ] filter: {
+[ ] categoryIds: {
+[ ] type: Array,
+[ ] index: true
+ },
+[ ] emotions: {
+[ ] type: Array,
+[ ] index: true
+ }
+ },
+[ ] hideUsersWithoutTermsOfUseSigniture: {type: Boolean},
+[ ] updatedAt: {
+[ ] type: Date,
+[ ] default: Date.now
+ }
+ }
+*/
+
+CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as usersetting;
diff --git a/Human-Connection/deployment/minikube/README.md b/Human-Connection/deployment/minikube/README.md
new file mode 100644
index 000000000..e77ddd667
--- /dev/null
+++ b/Human-Connection/deployment/minikube/README.md
@@ -0,0 +1,25 @@
+# Minikube
+
+There are many Kubernetes providers, but if you're just getting started, Minikube is a tool that you can use to get your feet wet.
+
+After you [installed Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/)
+open your minikube dashboard:
+
+```text
+$ minikube dashboard
+```
+
+This will give you an overview. Some of the steps below need some timing to make ressources available to other dependent deployments. Keeping an eye on the dashboard is a great way to check that.
+
+Follow the installation instruction for [Human Connection](../human-connection/README.md).
+If all the pods and services have settled and everything looks green in your
+minikube dashboard, expose the services you want on your host system.
+
+For example:
+
+```text
+$ minikube service nitro-web --namespace=human-connection
+# optionally
+$ minikube service nitro-backend --namespace=human-connection
+```
+
diff --git a/Human-Connection/deployment/namespace.yaml b/Human-Connection/deployment/namespace.yaml
new file mode 100644
index 000000000..0710da55b
--- /dev/null
+++ b/Human-Connection/deployment/namespace.yaml
@@ -0,0 +1,6 @@
+kind: Namespace
+apiVersion: v1
+metadata:
+ name: human-connection
+ labels:
+ name: human-connection
diff --git a/Human-Connection/deployment/volumes/README.md b/Human-Connection/deployment/volumes/README.md
new file mode 100644
index 000000000..2d08a34cb
--- /dev/null
+++ b/Human-Connection/deployment/volumes/README.md
@@ -0,0 +1,36 @@
+# Persistent Volumes
+
+At the moment, the application needs two persistent volumes:
+
+* The `/data/` folder where `neo4j` stores its database and
+* the folder `/nitro-backend/public/uploads` where the backend stores uploads.
+
+As a matter of precaution, the persistent volume claims that setup these volumes
+live in a separate folder. You don't want to accidently loose all your data in
+your database by running
+
+```sh
+kubectl delete -f human-connection/
+```
+
+or do you?
+
+## Create Persistent Volume Claims
+
+Run the following:
+```sh
+# in folder deployments/
+$ kubectl apply -f volumes
+persistentvolumeclaim/neo4j-data-claim created
+persistentvolumeclaim/uploads-claim created
+```
+
+## Backup and Restore
+
+We tested a couple of options how to do disaster recovery in kubernetes. First,
+there is the [offline backup strategy](./neo4j-offline-backup/README.md) of the
+community edition of Neo4J, which you can also run on a local installation.
+Kubernetes also offers so-called [volume snapshots](./volume-snapshots/README.md).
+Changing the [reclaim policy](./reclaim-policy/README.md) of your persistent
+volumes might be an additional safety measure. Finally, there is also a
+kubernetes specific disaster recovery tool called [Velero](./velero/README.md).
diff --git a/Human-Connection/deployment/volumes/neo4j-data.yaml b/Human-Connection/deployment/volumes/neo4j-data.yaml
new file mode 100644
index 000000000..f077be933
--- /dev/null
+++ b/Human-Connection/deployment/volumes/neo4j-data.yaml
@@ -0,0 +1,12 @@
+---
+ kind: PersistentVolumeClaim
+ apiVersion: v1
+ metadata:
+ name: neo4j-data-claim
+ namespace: human-connection
+ spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 1Gi
diff --git a/Human-Connection/deployment/volumes/neo4j-offline-backup/README.md b/Human-Connection/deployment/volumes/neo4j-offline-backup/README.md
new file mode 100644
index 000000000..3638ebc89
--- /dev/null
+++ b/Human-Connection/deployment/volumes/neo4j-offline-backup/README.md
@@ -0,0 +1,86 @@
+# Backup (offline)
+
+This tutorial explains how to carry out an offline backup of your Neo4J
+database in a kubernetes cluster.
+
+An offline backup requires the Neo4J database to be stopped. Read
+[the docs](https://neo4j.com/docs/operations-manual/current/tools/dump-load/).
+Neo4J also offers online backups but this is available in enterprise edition
+only.
+
+The tricky part is to stop the Neo4J database *without* stopping the container.
+Neo4J's docker container image starts `neo4j` by default, so we have to override
+this command with sth. that keeps the container spinning but does not terminate
+it.
+
+## Stop and Restart Neo4J Database in Kubernetes
+
+[This tutorial](http://bigdatums.net/2017/11/07/how-to-keep-docker-containers-running/)
+explains how to keep a docker container running. For kubernetes, the way to
+override the docker image `CMD` is explained [here](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#define-a-command-and-arguments-when-you-create-a-pod).
+
+So, all we have to do is edit the kubernetes deployment of our Neo4J database
+and set a custom `command` every time we have to carry out tasks like backup,
+restore, seed etc.
+
+{% hint style="info" %}
+TODO: implement maintenance mode
+{% endhint %}
+
+First bring the application into maintenance mode to ensure there are no
+database connections left and nobody can access the application.
+
+Run the following:
+
+```sh
+kubectl --namespace=human-connection edit deployment nitro-neo4j
+```
+
+Add the following to `spec.template.spec.containers`:
+```
+["tail", "-f", "/dev/null"]
+```
+and write the file which will update the deployment.
+
+The command `tail -f /dev/null` is the equivalent of *sleep forever*. It is a
+hack to keep the container busy and to prevent its shutdown. It will also
+override the default `neo4j` command and the kubernetes pod will not start the
+database.
+
+Now perform your tasks!
+
+When you're done, edit the deployment again and remove the `command`. Write the
+file and trigger an update of the deployment.
+
+## Create a Backup in Kubernetes
+
+First stop your Neo4J database, see above. Then:
+```sh
+kubectl --namespace=human-connection get pods
+# Copy the ID of the pod running Neo4J.
+kubectl --namespace=human-connection exec -it bash
+# Once you're in the pod, dump the db to a file e.g. `/root/neo4j-backup`.
+neo4j-admin dump --to=/root/neo4j-backup
+exit
+# Download the file from the pod to your computer.
+ kubectl cp human-connection/:/root/neo4j-backup ./neo4j-backup
+```
+Revert your changes to deployment `nitro-neo4j` which will restart the database.
+
+## Restore a Backup in Kubernetes
+
+First stop your Neo4J database. Then:
+```sh
+kubectl --namespace=human-connection get pods
+# Copy the ID of the pod running Neo4J.
+# Then upload your local backup to the pod. Note that once the pod gets deleted
+# e.g. if you change the deployment, the backup file is gone with it.
+kubectl cp ./neo4j-backup human-connection/:/root/
+kubectl --namespace=human-connection exec -it bash
+# Once you're in the pod restore the backup and overwrite the default database
+# called `graph.db` with `--force`.
+# This will delete all existing data in database `graph.db`!
+neo4j-admin load --from=/root/neo4j-backup --force
+exit
+```
+Revert your changes to deployment `nitro-neo4j` which will restart the database.
diff --git a/Human-Connection/deployment/volumes/reclaim-policy/README.md b/Human-Connection/deployment/volumes/reclaim-policy/README.md
new file mode 100644
index 000000000..00c91c319
--- /dev/null
+++ b/Human-Connection/deployment/volumes/reclaim-policy/README.md
@@ -0,0 +1,30 @@
+# Change Reclaim Policy
+
+We recommend to change the `ReclaimPolicy`, so if you delete the persistent
+volume claims, the associated volumes will be released, not deleted.
+
+This procedure is optional and an additional security measure. It might prevent
+you from loosing data if you accidently delete the namespace and the persistent
+volumes along with it.
+
+```sh
+$ kubectl --namespace=human-connection get pv
+
+NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
+pvc-bd02a715-66d0-11e9-be52-ba9c337f4551 1Gi RWO Delete Bound human-connection/neo4j-data-claim do-block-storage 4m24s
+pvc-bd208086-66d0-11e9-be52-ba9c337f4551 2Gi RWO Delete Bound human-connection/uploads-claim do-block-storage 4m12s
+```
+
+Get the volume id from above, then change `ReclaimPolicy` with:
+```sh
+kubectl patch pv -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
+
+# in the above example
+kubectl patch pv pvc-bd02a715-66d0-11e9-be52-ba9c337f4551 -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
+kubectl patch pv pvc-bd208086-66d0-11e9-be52-ba9c337f4551 -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
+```
+
+Given that you changed the reclaim policy as described above, you should be able
+to create a persistent volume claim based on a volume snapshot content. See
+the general kubernetes documentation [here](https://kubernetes.io/blog/2018/10/09/introducing-volume-snapshot-alpha-for-kubernetes/)
+and our specific documentation for snapshots [here](../snapshot/README.md).
diff --git a/Human-Connection/deployment/volumes/uploads.yaml b/Human-Connection/deployment/volumes/uploads.yaml
new file mode 100644
index 000000000..2bd64c9ee
--- /dev/null
+++ b/Human-Connection/deployment/volumes/uploads.yaml
@@ -0,0 +1,12 @@
+---
+ kind: PersistentVolumeClaim
+ apiVersion: v1
+ metadata:
+ name: uploads-claim
+ namespace: human-connection
+ spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 25Gi
diff --git a/Human-Connection/deployment/volumes/velero/README.md b/Human-Connection/deployment/volumes/velero/README.md
new file mode 100644
index 000000000..e469ad117
--- /dev/null
+++ b/Human-Connection/deployment/volumes/velero/README.md
@@ -0,0 +1,112 @@
+# Velero
+
+{% hint style="danger" %}
+I tried Velero and it did not work reliably all the time. Sometimes the
+kubernetes cluster crashes during recovery or data is not fully recovered.
+
+Feel free to test it out and update this documentation once you feel that it's
+working reliably. It is very likely that Digital Ocean had some bugs when I
+tried out the steps below.
+{% endhint %}
+
+We use [velero](https://github.com/heptio/velero) for on premise backups, we
+tested on version `v0.11.0`, you can find their
+documentation [here](https://heptio.github.io/velero/v0.11.0/).
+
+Our kubernets configurations adds some annotations to pods. The annotations
+define the important persistent volumes that need to be backed up. Velero will
+pick them up and store the volumes in the same cluster but in another namespace
+`velero`.
+
+## Prequisites
+
+You have to install the binary `velero` on your computer and get a tarball of
+the latest release. We use `v0.11.0` so visit the
+[release](https://github.com/heptio/velero/releases/tag/v0.11.0) page and
+download and extract e.g. [velero-v0.11.0-linux-arm64.tar.gz](https://github.com/heptio/velero/releases/download/v0.11.0/velero-v0.11.0-linux-amd64.tar.gz).
+
+
+## Setup Velero Namespace
+
+Follow their [getting started](https://heptio.github.io/velero/v0.11.0/get-started)
+instructions to setup the Velero namespace. We use
+[Minio](https://docs.min.io/docs/deploy-minio-on-kubernetes) and
+[restic](https://github.com/restic/restic), so check out Velero's instructions
+how to setup [restic](https://heptio.github.io/velero/v0.11.0/restic):
+
+```sh
+# run from the extracted folder of the tarball
+$ kubectl apply -f config/common/00-prereqs.yaml
+$ kubectl apply -f config/minio/
+```
+
+Once completed, you should see the namespace in your kubernetes dashboard.
+
+## Manually Create an On-Premise Backup
+
+When you create your deployments for Human Connection the required annotations
+should already be in place. So when you create a backup of namespace
+`human-connection`:
+
+```sh
+$ velero backup create hc-backup --include-namespaces=human-connection
+```
+
+That should backup your persistent volumes, too. When you enter:
+
+```
+$ velero backup describe hc-backup --details
+```
+
+You should see the persistent volumes at the end of the log:
+
+```
+....
+
+Restic Backups:
+ Completed:
+ human-connection/nitro-backend-5b6dd96d6b-q77n6: uploads
+ human-connection/nitro-neo4j-686d768598-z2vhh: neo4j-data
+```
+
+## Simulate a Disaster
+
+Feel free to try out if you loose any data when you simulate a disaster and try
+to restore the namespace from the backup:
+
+```sh
+$ kubectl delete namespace human-connection
+```
+
+Wait until the wrongdoing has completed, then:
+```sh
+$ velero restore create --from-backup hc-backup
+```
+
+Now, I keep my fingers crossed that everything comes back again. If not, I feel
+very sorry for you.
+
+
+## Schedule a Regular Backup
+
+Check out the [docs](https://heptio.github.io/velero/v0.11.0/get-started). You
+can create a regular schedule e.g. with:
+
+```sh
+$ velero schedule create hc-weekly-backup --schedule="@weekly" --include-namespaces=human-connection
+```
+
+Inspect the created backups:
+
+```sh
+$ velero schedule get
+NAME STATUS CREATED SCHEDULE BACKUP TTL LAST BACKUP SELECTOR
+hc-weekly-backup Enabled 2019-05-08 17:51:31 +0200 CEST @weekly 720h0m0s 6s ago
+
+$ velero backup get
+NAME STATUS CREATED EXPIRES STORAGE LOCATION SELECTOR
+hc-weekly-backup-20190508155132 Completed 2019-05-08 17:51:32 +0200 CEST 29d default
+
+$ velero backup describe hc-weekly-backup-20190508155132 --details
+# see if the persistent volumes are backed up
+```
diff --git a/Human-Connection/deployment/volumes/volume-snapshots/README.md b/Human-Connection/deployment/volumes/volume-snapshots/README.md
new file mode 100644
index 000000000..cc66ae4ae
--- /dev/null
+++ b/Human-Connection/deployment/volumes/volume-snapshots/README.md
@@ -0,0 +1,50 @@
+# Kubernetes Volume Snapshots
+
+It is possible to backup persistent volumes through volume snapshots. This is
+especially handy if you don't want to stop the database to create an [offline
+backup](../neo4j-offline-backup/README.md) thus having a downtime.
+
+Kubernetes announced this feature in a [blog post](https://kubernetes.io/blog/2018/10/09/introducing-volume-snapshot-alpha-for-kubernetes/). Please make yourself familiar with it before you continue.
+
+## Create a Volume Snapshot
+
+There is an example in this folder how you can e.g. create a volume snapshot for
+the persistent volume claim `neo4j-data-claim`:
+
+```sh
+# in folder deployment/volumes/volume-snapshots/
+kubectl apply -f snapshot.yaml
+```
+
+If you are on Digital Ocean the volume snapshot should show up in the Web UI:
+
+
+
+## Provision a Volume based on a Snapshot
+
+Edit your persistent volume claim configuration and add a `dataSource` pointing
+to your volume snapshot. [The blog post](https://kubernetes.io/blog/2018/10/09/introducing-volume-snapshot-alpha-for-kubernetes/) has an example in section "Provision a new volume from a snapshot with
+Kubernetes".
+
+There is also an example in this folder how the configuration could look like.
+If you apply the configuration new persistent volume claim will be provisioned
+with the data from the volume snapshot:
+
+```
+# in folder deployment/volumes/volume-snapshots/
+kubectl apply -f neo4j-data.yaml
+```
+
+## Data Consistency Warning
+
+Note that volume snapshots do not guarantee data consistency. Quote from the
+[blog post](https://kubernetes.io/blog/2018/10/09/introducing-volume-snapshot-alpha-for-kubernetes/):
+
+> Please note that the alpha release of Kubernetes Snapshot does not provide
+> any consistency guarantees. You have to prepare your application (pause
+> application, freeze filesystem etc.) before taking the snapshot for data
+> consistency.
+
+In case of Neo4J this probably means that enterprise edition is required which
+supports [online backups](https://neo4j.com/docs/operations-manual/current/backup/).
+
diff --git a/Human-Connection/deployment/volumes/volume-snapshots/digital-ocean-volume-snapshots.png b/Human-Connection/deployment/volumes/volume-snapshots/digital-ocean-volume-snapshots.png
new file mode 100644
index 000000000..cb6599616
Binary files /dev/null and b/Human-Connection/deployment/volumes/volume-snapshots/digital-ocean-volume-snapshots.png differ
diff --git a/Human-Connection/deployment/volumes/volume-snapshots/neo4j-data.yaml b/Human-Connection/deployment/volumes/volume-snapshots/neo4j-data.yaml
new file mode 100644
index 000000000..7de9e19dc
--- /dev/null
+++ b/Human-Connection/deployment/volumes/volume-snapshots/neo4j-data.yaml
@@ -0,0 +1,18 @@
+---
+ kind: PersistentVolumeClaim
+ apiVersion: v1
+ metadata:
+ name: neo4j-data-claim
+ namespace: human-connection
+ labels:
+ app: human-connection
+ spec:
+ dataSource:
+ name: neo4j-data-snapshot
+ kind: VolumeSnapshot
+ apiGroup: snapshot.storage.k8s.io
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 1Gi
diff --git a/Human-Connection/deployment/volumes/volume-snapshots/snapshot.yaml b/Human-Connection/deployment/volumes/volume-snapshots/snapshot.yaml
new file mode 100644
index 000000000..3c3487e14
--- /dev/null
+++ b/Human-Connection/deployment/volumes/volume-snapshots/snapshot.yaml
@@ -0,0 +1,10 @@
+---
+ apiVersion: snapshot.storage.k8s.io/v1alpha1
+ kind: VolumeSnapshot
+ metadata:
+ name: neo4j-data-snapshot
+ namespace: human-connection
+ spec:
+ source:
+ name: neo4j-data-claim
+ kind: PersistentVolumeClaim
diff --git a/Human-Connection/docker-compose.maintenance.yml b/Human-Connection/docker-compose.maintenance.yml
new file mode 100644
index 000000000..e536b1157
--- /dev/null
+++ b/Human-Connection/docker-compose.maintenance.yml
@@ -0,0 +1,45 @@
+version: "3.4"
+
+services:
+ maintenance:
+ image: humanconnection/maintenance-worker:latest
+ build:
+ context: deployment/legacy-migration/maintenance-worker
+ volumes:
+ - uploads:/uploads
+ - neo4j-data:/data
+ - ./deployment/legacy-migration/maintenance-worker/migration/:/migration
+ - ./deployment/legacy-migration/maintenance-worker/ssh/:/root/.ssh
+ networks:
+ - hc-network
+ environment:
+ - NEO4J_dbms_security_auth__enabled=false
+ - NEO4J_dbms_memory_heap_max__size=2G
+ - GRAPHQL_PORT=4000
+ - GRAPHQL_URI=http://localhost:4000
+ - CLIENT_URI=http://localhost:3000
+ - JWT_SECRET=b/&&7b78BF&fv/Vd
+ - MOCKS=false
+ - MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ
+ - PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78
+ - NEO4J_apoc_import_file_enabled=true
+ - "SSH_USERNAME=${SSH_USERNAME}"
+ - "SSH_HOST=${SSH_HOST}"
+ - "MONGODB_USERNAME=${MONGODB_USERNAME}"
+ - "MONGODB_PASSWORD=${MONGODB_PASSWORD}"
+ - "MONGODB_AUTH_DB=${MONGODB_AUTH_DB}"
+ - "MONGODB_DATABASE=${MONGODB_DATABASE}"
+ - "UPLOADS_DIRECTORY=${UPLOADS_DIRECTORY}"
+ - "MONGO_EXPORT_SPLIT_SIZE=${MONGO_EXPORT_SPLIT_SIZE}"
+ ports:
+ - 7687:7687
+ - 7474:7474
+
+networks:
+ hc-network:
+
+volumes:
+ webapp_node_modules:
+ backend_node_modules:
+ neo4j-data:
+ uploads:
diff --git a/Human-Connection/docker-compose.override.yml b/Human-Connection/docker-compose.override.yml
new file mode 100644
index 000000000..016984d3b
--- /dev/null
+++ b/Human-Connection/docker-compose.override.yml
@@ -0,0 +1,46 @@
+version: "3.4"
+
+services:
+ mailserver:
+ image: djfarrelly/maildev
+ ports:
+ - 1080:80
+ networks:
+ - hc-network
+ webapp:
+ build:
+ context: webapp
+ target: build-and-test
+ volumes:
+ - ./webapp:/nitro-web
+ - webapp_node_modules:/nitro-web/node_modules
+ command: yarn run dev
+ user: root
+ backend:
+ image: humanconnection/nitro-backend:builder
+ build:
+ context: backend
+ target: builder
+ volumes:
+ - ./backend:/nitro-backend
+ - backend_node_modules:/nitro-backend/node_modules
+ - uploads:/nitro-backend/public/uploads
+ command: yarn run dev
+ environment:
+ - SMTP_HOST=mailserver
+ - SMTP_PORT=25
+ - SMTP_IGNORE_TLS=true
+ neo4j:
+ environment:
+ - NEO4J_AUTH=none
+ ports:
+ - 7687:7687
+ - 7474:7474
+ volumes:
+ - neo4j-data:/data
+
+volumes:
+ webapp_node_modules:
+ backend_node_modules:
+ neo4j-data:
+ uploads:
diff --git a/Human-Connection/docker-compose.travis.yml b/Human-Connection/docker-compose.travis.yml
new file mode 100644
index 000000000..bc627a67a
--- /dev/null
+++ b/Human-Connection/docker-compose.travis.yml
@@ -0,0 +1,34 @@
+version: "3.4"
+
+services:
+ neo4j:
+ environment:
+ - NEO4J_AUTH=none
+ ports:
+ - 7687:7687
+ - 7474:7474
+ webapp:
+ build:
+ context: webapp
+ target: build-and-test
+ volumes:
+ #/nitro-web
+ - ./webapp/coverage:/nitro-web/coverage
+ environment:
+ - GRAPHQL_URI=http://backend:4000
+ backend:
+ image: humanconnection/nitro-backend:builder
+ build:
+ context: backend
+ target: builder
+ volumes:
+ - ./backend/coverage:/nitro-backend/coverage
+ ports:
+ - 4001:4001
+ - 4123:4123
+ neo4j:
+ environment:
+ - NEO4J_AUTH=none
+ ports:
+ - 7687:7687
+ - 7474:7474
diff --git a/Human-Connection/docker-compose.yml b/Human-Connection/docker-compose.yml
new file mode 100644
index 000000000..ca66217c2
--- /dev/null
+++ b/Human-Connection/docker-compose.yml
@@ -0,0 +1,46 @@
+version: "3.4"
+
+services:
+ webapp:
+ image: humanconnection/nitro-web:latest
+ build:
+ context: webapp
+ target: production
+ ports:
+ - 3000:3000
+ - 8080:8080
+ networks:
+ - hc-network
+ environment:
+ - HOST=0.0.0.0
+ - GRAPHQL_URI=http://backend:4000
+ - MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.bZ8KK9l70omjXbEkkbHGsQ"
+ backend:
+ image: humanconnection/nitro-backend:latest
+ build:
+ context: backend
+ target: production
+ networks:
+ - hc-network
+ depends_on:
+ - neo4j
+ ports:
+ - 4000:4000
+ environment:
+ - NEO4J_URI=bolt://neo4j:7687
+ - GRAPHQL_PORT=4000
+ - GRAPHQL_URI=http://localhost:4000
+ - CLIENT_URI=http://localhost:3000
+ - JWT_SECRET=b/&&7b78BF&fv/Vd
+ - MOCKS=false
+ - MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ
+ - PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78
+ neo4j:
+ image: humanconnection/neo4j:latest
+ build:
+ context: neo4j
+ networks:
+ - hc-network
+
+networks:
+ hc-network:
diff --git a/Human-Connection/edit-this-documentation.md b/Human-Connection/edit-this-documentation.md
new file mode 100644
index 000000000..b01ace78f
--- /dev/null
+++ b/Human-Connection/edit-this-documentation.md
@@ -0,0 +1,120 @@
+# Edit this Documentation
+
+Go to the section and theme you want to change: On the left navigator.
+
+Click **Edit on GitHub** on the right.
+
+On the **Issue** tab you’ll find the open issues. Read what need to be done by clicking on the issue you like to fix.
+
+By going backwards in the browser **\(!\)**, again go to the **Code** tab.
+
+Click on the **edit pencil** on the right side directly above the text to edit this file on your fork of Human Connection \(HC\).
+
+You can see a preview of your changes by clicking the **Preview changes** tab aside the **Edit file** tab.
+
+If you are ready, fill in the **Propose file change** at the end of the webpage.
+
+After that you have to send your change to the HC basis with a pull request. Here make a comment which issue you have fixed. At least the number.
+
+## Markdown your documentation
+
+To design your documentation see the syntax description at GitBook:
+
+[https://toolchain.gitbook.com/syntax/markdown.html](https://toolchain.gitbook.com/syntax/markdown.html)
+
+### Some quick Examples
+
+#### Headlines
+
+```text
+# Main headline
+## Smaller headlines
+### Small headlines
+```
+
+#### Tabs
+
+```text
+{% tabs %}
+{% tab title="XXX" %}
+XXX
+{% endtab %}
+{% tab title="XXX" %}
+XXX
+{% endtab %}
+…
+{% endtabs %}
+```
+
+#### Commands
+
+```text
+```LANGUAGE (for text highlighting)
+XXX
+```
+
+```text
+#### Links
+
+```text
+[https://XXX](XXX)
+```
+
+#### Screenshots or other Images
+
+```text
+
+```
+
+#### Hints for ToDos
+
+```text
+{% hint style="info" %} TODO: XXX {% endhint %}
+```
+
+## Host the screenshots
+
+### Host on Human Connection
+
+{% hint style="info" %}
+TODO: How to host on Human Connection \(GitHub\) ...
+{% endhint %}
+
+### Quick Solution
+
+To quickly host the screenshots go to:
+
+[https://imgur.com](https://imgur.com).
+
+There click the green button **New post**.
+
+Drag the image into the appropriate area.
+
+Right click on it and choose kind of **Open link in new tab**.
+
+Copy the URL and paste it were you need it.
+
+## Screenshot modification
+
+### Add an arrow or some other marking stuff
+
+{% tabs %}
+{% tab title="macOS" %}
+#### In the Preview App
+
+Got to: **Menu** + **Tools** \(GER: Werkzeuge\) + **Annotate** \(GER: Anmerkungen\) + etc.
+{% endtab %}
+
+{% tab title="Windows" %}
+{% hint style="info" %}
+TODO: How to modify screenshots in Windows ...
+{% endhint %}
+{% endtab %}
+
+{% tab title="Linux" %}
+{% hint style="info" %}
+TODO: How to modify screenshots in Linux ...
+{% endhint %}
+{% endtab %}
+{% endtabs %}
+
diff --git a/Human-Connection/installation.md b/Human-Connection/installation.md
new file mode 100644
index 000000000..37531f95d
--- /dev/null
+++ b/Human-Connection/installation.md
@@ -0,0 +1,82 @@
+# Installation
+
+The repository can be found on GitHub. [https://github.com/Human-Connection/Human-Connection](https://github.com/Human-Connection/Human-Connection)
+
+We give write permissions to every developer who asks for it. Just text us on
+[Discord](https://discord.gg/6ub73U3).
+
+## Clone the Repository
+
+
+Clone the repository, this will create a new folder called `Human-Connection`:
+
+{% tabs %}
+{% tab title="HTTPS" %}
+```bash
+$ git clone https://github.com/Human-Connection/Human-Connection.git
+```
+{% endtab %}
+
+{% tab title="SSH" %}
+```bash
+$ git clone git@github.com:Human-Connection/Human-Connection.git
+```
+{% endtab %}
+{% endtabs %}
+
+Change into the new folder.
+
+```bash
+$ cd Human-Connection
+```
+
+## Directory Layout
+
+There are four important directories:
+* [Backend](./backend) runs on the server and is a middleware between database and frontend
+* [Frontend](./webapp) is a server-side-rendered and client-side-rendered web frontend
+* [Deployment](./deployment) configuration for kubernetes
+* [Cypress](./cypress) contains end-to-end tests and executable feature specifications
+
+In order to setup the application and start to develop features you have to
+setup **frontend** and **backend**.
+
+There are two approaches:
+
+1. Local installation, which means you have to take care of dependencies yourself
+2. **Or** Install everything through docker which takes care of dependencies for you
+
+## Docker Installation
+
+Docker is a software development container tool that combines software and its dependencies into one standardized unit that contains everything needed to run it. This helps us to avoid problems with dependencies and makes installation easier.
+
+### General Installation of Docker
+
+There are [sevaral ways to install Docker CE](https://docs.docker.com/install/) on your computer or server.
+
+{% tabs %}
+{% tab title="Docker Desktop macOS" %}
+Follow these instructions to [install Docker Desktop on macOS](https://docs.docker.com/docker-for-mac/install/).
+{% endtab %}
+
+{% tab title="Docker Desktop Windows" %}
+Follow these instructions to [install Docker Desktop on Windows](https://docs.docker.com/docker-for-windows/install/).
+{% endtab %}
+
+{% tab title="Docker CE" %}
+Follow these instructions to [install Docker CE](https://docs.docker.com/install/).
+
+This is a great option for Linux users.
+{% endtab %}
+{% endtabs %}
+
+Check the correct Docker installation by checking the version before proceeding. E.g. we have the following versions:
+
+```bash
+$ docker --version
+Docker version 18.09.2
+$ docker-compose --version
+docker-compose version 1.23.2
+```
+
+
diff --git a/Human-Connection/neo4j/.env.template b/Human-Connection/neo4j/.env.template
new file mode 100644
index 000000000..c58edee0e
--- /dev/null
+++ b/Human-Connection/neo4j/.env.template
@@ -0,0 +1,2 @@
+NEO4J_USERNAME=neo4j
+NEO4J_PASSWORD=letmein
diff --git a/Human-Connection/neo4j/.gitignore b/Human-Connection/neo4j/.gitignore
new file mode 100644
index 000000000..4c49bd78f
--- /dev/null
+++ b/Human-Connection/neo4j/.gitignore
@@ -0,0 +1 @@
+.env
diff --git a/Human-Connection/neo4j/Dockerfile b/Human-Connection/neo4j/Dockerfile
new file mode 100644
index 000000000..2c106882f
--- /dev/null
+++ b/Human-Connection/neo4j/Dockerfile
@@ -0,0 +1,11 @@
+FROM neo4j:3.5.5
+LABEL Description="Neo4J database of the Social Network Human-Connection.org with preinstalled database constraints and indices" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
+
+ARG BUILD_COMMIT
+ENV BUILD_COMMIT=$BUILD_COMMIT
+
+RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.1/apoc-3.5.0.1-all.jar -P plugins/
+RUN apk add --no-cache --quiet procps
+COPY db_setup.sh /usr/local/bin/db_setup
+COPY entrypoint.sh /docker-entrypoint-wrapper.sh
+ENTRYPOINT ["/docker-entrypoint-wrapper.sh"]
diff --git a/Human-Connection/neo4j/README.md b/Human-Connection/neo4j/README.md
new file mode 100644
index 000000000..379a89eec
--- /dev/null
+++ b/Human-Connection/neo4j/README.md
@@ -0,0 +1,64 @@
+# Neo4J
+
+Human Connection is a social network. Using a graph based database which can
+model nodes and edges natively - a network - feels like an obvious choice. We
+decided to use [Neo4j](https://neo4j.com/), the currently most used graph
+database available. The community edition of Neo4J is Free and Open Source and
+we try our best to keep our application compatible with the community edition
+only.
+
+## Installation with Docker
+
+Run:
+
+```bash
+docker-compose up
+```
+
+You can access Neo4J through [http://localhost:7474/](http://localhost:7474/)
+for an interactive cypher shell and a visualization of the graph.
+
+## Installation without Docker
+
+Install community edition of [Neo4J]() along with the plugin
+[Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures) on your system.
+
+To do so, go to [releases](https://neo4j.com/download-center/#releases), choose
+"Community Server", download the installation files for you operation system
+and unpack the files.
+
+Download [Neo4j Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases)
+and drop the file into the `plugins` folder of the just extracted Neo4j-Server.
+
+### Alternatives
+
+You can download [Neo4j Desktop](https://neo4j.com/download/) and run locally
+for development, spin up a
+[hosted Neo4j Sandbox instance](https://neo4j.com/download/), run Neo4j in one
+of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/),
+[spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/),
+on Archlinux you can install [neo4j-community from AUR](https://aur.archlinux.org/packages/neo4j-community/)
+or on Debian-based systems install [Neo4j from the Debian Repository](http://debian.neo4j.org/).
+Just be sure to update the Neo4j connection string and credentials accordingly
+in `backend/.env`.
+
+Start Neo4J and confirm the database is running at [http://localhost:7474](http://localhost:7474).
+
+## Database Indices and Constraints
+
+If you are not running our dedicated Neo4J [docker image](https://hub.docker.com/r/humanconnection/neo4j),
+which is the case if you setup Neo4J locally without docker, then you have to
+setup unique indices and database constraints manually.
+
+If you have `cypher-shell` available with your local installation of neo4j you
+can run:
+
+```bash
+# in folder neo4j/
+$ cp .env.template .env
+$ ./db_setup.sh
+```
+
+Otherwise if you don't have `cypher-shell` available, simply copy the cypher
+statements [from the script](./neo4j/db_setup.sh) and paste the scripts into your
+database [browser frontend](http://localhost:7474).
diff --git a/Human-Connection/neo4j/db_setup.sh b/Human-Connection/neo4j/db_setup.sh
new file mode 100755
index 000000000..21ed54571
--- /dev/null
+++ b/Human-Connection/neo4j/db_setup.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+
+ENV_FILE=$(dirname "$0")/.env
+[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
+
+if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then
+ echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables."
+ echo "Setting up database constraints and indexes will probably fail because of authentication errors."
+ echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container"
+fi
+
+until echo 'RETURN "Connection successful" as info;' | cypher-shell
+do
+ echo "Connecting to neo4j failed, trying again..."
+ sleep 1
+done
+
+echo '
+RETURN "Here is a list of indexes and constraints BEFORE THE SETUP:" as info;
+CALL db.indexes();
+' | cypher-shell
+
+echo '
+CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]);
+CREATE CONSTRAINT ON (p:Post) ASSERT p.id IS UNIQUE;
+CREATE CONSTRAINT ON (c:Comment) ASSERT c.id IS UNIQUE;
+CREATE CONSTRAINT ON (c:Category) ASSERT c.id IS UNIQUE;
+CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE;
+CREATE CONSTRAINT ON (o:Organization) ASSERT o.id IS UNIQUE;
+CREATE CONSTRAINT ON (t:Tag) ASSERT t.id IS UNIQUE;
+
+
+CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE;
+CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE;
+CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE;
+CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE;
+' | cypher-shell
+
+echo '
+RETURN "Setting up all the indexes and constraints seems to have been successful. Here is a list AFTER THE SETUP:" as info;
+CALL db.indexes();
+' | cypher-shell
diff --git a/Human-Connection/neo4j/entrypoint.sh b/Human-Connection/neo4j/entrypoint.sh
new file mode 100755
index 000000000..f9c1afbe1
--- /dev/null
+++ b/Human-Connection/neo4j/entrypoint.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# credits: https://github.com/javamonkey79
+# https://github.com/neo4j/docker-neo4j/issues/166
+
+# turn on bash's job control
+set -m
+
+# Start the primary process and put it in the background
+/docker-entrypoint.sh neo4j &
+
+# Start the helper process
+db_setup
+
+# the my_helper_process might need to know how to wait on the
+# primary process to start before it does its work and returns
+
+
+# now we bring the primary process back into the foreground
+# and leave it there
+fg %1
diff --git a/Human-Connection/package.json b/Human-Connection/package.json
new file mode 100644
index 000000000..1446f0009
--- /dev/null
+++ b/Human-Connection/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "nitro-cypress",
+ "version": "1.0.0",
+ "description": "Fullstack tests with cypress for Human Connection",
+ "author": "Human Connection gGmbh",
+ "license": "MIT",
+ "cypress-cucumber-preprocessor": {
+ "nonGlobalStepDefinitions": true
+ },
+ "scripts": {
+ "db:seed": "cd backend && yarn run db:seed",
+ "db:reset": "cd backend && yarn run db:reset",
+ "cypress:backend:server": "cd backend && yarn run test:before:server",
+ "cypress:backend:seeder": "cd backend && yarn run test:before:seeder",
+ "cypress:webapp": "cd webapp && cross-env GRAPHQL_URI=http://localhost:4123 yarn run dev",
+ "cypress:setup": "run-p cypress:backend:* cypress:webapp",
+ "cypress:run": "cypress run --browser chromium",
+ "cypress:open": "cypress open --browser chromium",
+ "test:jest": "cd webapp && yarn test && cd ../backend && yarn test:jest && codecov"
+ },
+ "devDependencies": {
+ "codecov": "^3.5.0",
+ "cross-env": "^5.2.0",
+ "cypress": "^3.3.2",
+ "cypress-cucumber-preprocessor": "^1.12.0",
+ "cypress-file-upload": "^3.2.0",
+ "cypress-plugin-retries": "^1.2.2",
+ "dotenv": "^8.0.0",
+ "faker": "Marak/faker.js#master",
+ "graphql-request": "^1.8.2",
+ "neo4j-driver": "^1.7.5",
+ "npm-run-all": "^4.1.5"
+ }
+}
diff --git a/Human-Connection/scripts/deploy.sh b/Human-Connection/scripts/deploy.sh
new file mode 100755
index 000000000..b49fd68a2
--- /dev/null
+++ b/Human-Connection/scripts/deploy.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+sed -i "s//${TRAVIS_COMMIT}/g" $TRAVIS_BUILD_DIR/scripts/patch-deployment.yaml
+kubectl --namespace=human-connection patch deployment nitro-backend -p "$(cat $TRAVIS_BUILD_DIR/scripts/patch-deployment.yaml)"
+kubectl --namespace=human-connection patch deployment nitro-web -p "$(cat $TRAVIS_BUILD_DIR/scripts/patch-deployment.yaml)"
diff --git a/Human-Connection/scripts/docker_push.sh b/Human-Connection/scripts/docker_push.sh
new file mode 100755
index 000000000..c70367005
--- /dev/null
+++ b/Human-Connection/scripts/docker_push.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
+docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-backend:latest $TRAVIS_BUILD_DIR/backend
+docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-web:latest $TRAVIS_BUILD_DIR/webapp
+docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT -t humanconnection/neo4j:latest $TRAVIS_BUILD_DIR/neo4j
+docker build -t humanconnection/maintenance-worker:latest $TRAVIS_BUILD_DIR/deployment/legacy-migration/maintenance-worker
+docker push humanconnection/nitro-backend:latest
+docker push humanconnection/nitro-web:latest
+docker push humanconnection/neo4j:latest
+docker push humanconnection/maintenance-worker:latest
\ No newline at end of file
diff --git a/Human-Connection/scripts/patch-deployment.yaml b/Human-Connection/scripts/patch-deployment.yaml
new file mode 100644
index 000000000..c229b8e7c
--- /dev/null
+++ b/Human-Connection/scripts/patch-deployment.yaml
@@ -0,0 +1,5 @@
+spec:
+ template:
+ metadata:
+ labels:
+ human-connection.org/commit:
diff --git a/Human-Connection/scripts/setup_kubernetes.sh b/Human-Connection/scripts/setup_kubernetes.sh
new file mode 100755
index 000000000..2596a3e51
--- /dev/null
+++ b/Human-Connection/scripts/setup_kubernetes.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+# This script can be called multiple times for each `before_deploy` hook
+# so let's exit successfully if kubectl is already installed:
+command -v kubectl && exit 0
+
+curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
+chmod +x ./kubectl
+sudo mv ./kubectl /usr/local/bin/kubectl
+
+curl -LO https://github.com/digitalocean/doctl/releases/download/v1.14.0/doctl-1.14.0-linux-amd64.tar.gz
+tar xf doctl-1.14.0-linux-amd64.tar.gz
+chmod +x ./doctl
+sudo mv ./doctl /usr/local/bin/doctl
+
+doctl auth init --access-token $DOCTL_ACCESS_TOKEN
+mkdir -p ~/.kube/
+doctl kubernetes cluster kubeconfig show nitro-staging > ~/.kube/config
diff --git a/Human-Connection/testing.md b/Human-Connection/testing.md
new file mode 100644
index 000000000..2414c748f
--- /dev/null
+++ b/Human-Connection/testing.md
@@ -0,0 +1,20 @@
+# Testing Guide
+
+## End-to-End Testing
+
+To test all the pieces together, from the user perspective, we use integration tests. They also show if the the backend and the frontend are working as expected in conjunction and also if the browser likes our app.
+
+[more...](cypress/README.md)
+
+## Component Testing
+
+Individual Vue Components should also be documented and tested properly. This guarantees that they are reusable and the api gets more solid in the process.
+
+[more...](webapp/testing.md)
+
+## Unit Testing
+
+Expecially the Backend relies on Unit Tests, as there are no Vue Components.
+
+[more...](backend/testing.md)
+
diff --git a/Human-Connection/webapp/.babelrc b/Human-Connection/webapp/.babelrc
new file mode 100644
index 000000000..b23873e12
--- /dev/null
+++ b/Human-Connection/webapp/.babelrc
@@ -0,0 +1,27 @@
+{
+ "plugins": [
+ "@babel/plugin-syntax-dynamic-import"
+ ],
+ "presets": [
+ [
+ "@babel/preset-env",
+ {
+ "modules": false
+ }
+ ]
+ ],
+ "env": {
+ "test": {
+ "presets": [
+ [
+ "@babel/preset-env",
+ {
+ "targets": {
+ "node": "10"
+ }
+ }
+ ]
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Human-Connection/webapp/.dockerignore b/Human-Connection/webapp/.dockerignore
new file mode 100644
index 000000000..b0980ccfc
--- /dev/null
+++ b/Human-Connection/webapp/.dockerignore
@@ -0,0 +1,18 @@
+.vscode/
+
+styleguide/
+node_modules/
+npm-debug.log
+
+Dockerfile
+docker-compose*.yml
+scripts/
+
+.env
+
+cypress/
+
+README.md
+screenshot*.png
+lokalise.png
+.editorconfig
diff --git a/Human-Connection/webapp/.editorconfig b/Human-Connection/webapp/.editorconfig
new file mode 100644
index 000000000..5d1263484
--- /dev/null
+++ b/Human-Connection/webapp/.editorconfig
@@ -0,0 +1,13 @@
+# editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/Human-Connection/webapp/.env.template b/Human-Connection/webapp/.env.template
new file mode 100644
index 000000000..1fa2a542a
--- /dev/null
+++ b/Human-Connection/webapp/.env.template
@@ -0,0 +1 @@
+MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"
diff --git a/Human-Connection/webapp/.eslintignore b/Human-Connection/webapp/.eslintignore
new file mode 100644
index 000000000..d56900caf
--- /dev/null
+++ b/Human-Connection/webapp/.eslintignore
@@ -0,0 +1,5 @@
+node_modules
+build
+.nuxt
+styleguide/
+**/*.min.js
diff --git a/Human-Connection/webapp/.eslintrc.js b/Human-Connection/webapp/.eslintrc.js
new file mode 100644
index 000000000..f5aa82c81
--- /dev/null
+++ b/Human-Connection/webapp/.eslintrc.js
@@ -0,0 +1,33 @@
+module.exports = {
+ root: true,
+ env: {
+ browser: true,
+ node: true,
+ jest: true
+ },
+ parserOptions: {
+ parser: 'babel-eslint'
+ },
+ extends: [
+ 'standard',
+ 'plugin:vue/essential',
+ 'plugin:prettier/recommended'
+ ],
+ // required to lint *.vue files
+ plugins: [
+ 'vue',
+ 'prettier',
+ 'jest'
+ ],
+ // add your custom rules here
+ rules: {
+ //'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+ 'no-console': ['error'],
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+ 'vue/component-name-in-template-casing': ['error', 'kebab-case'],
+ 'prettier/prettier': ['error', {
+ htmlWhitespaceSensitivity: 'ignore'
+ }],
+ // 'newline-per-chained-call': [2]
+ }
+}
diff --git a/Human-Connection/webapp/.gitignore b/Human-Connection/webapp/.gitignore
new file mode 100644
index 000000000..f8c980f7c
--- /dev/null
+++ b/Human-Connection/webapp/.gitignore
@@ -0,0 +1,86 @@
+# Created by .ignore support plugin (hsz.mobi)
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+styleguide/
+
+# TypeScript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# next.js build output
+.next
+
+# nuxt.js build output
+.nuxt
+
+# Nuxt generate
+dist
+
+#ignore internal github files
+/.github
+
+# Serverless directories
+.serverless
+
+# IDE
+.idea
+.vscode
+
+# TEMORIRY
+static/uploads
+
+cypress/videos
+cypress/screenshots/
+cypress.env.json
+
+# Apple macOS folder attribute file
+.DS_Store
diff --git a/Human-Connection/webapp/.prettierrc.js b/Human-Connection/webapp/.prettierrc.js
new file mode 100644
index 000000000..e2cf91e91
--- /dev/null
+++ b/Human-Connection/webapp/.prettierrc.js
@@ -0,0 +1,9 @@
+
+module.exports = {
+ semi: false,
+ printWidth: 100,
+ singleQuote: true,
+ trailingComma: "all",
+ tabWidth: 2,
+ bracketSpacing: true
+};
diff --git a/Human-Connection/webapp/Dockerfile b/Human-Connection/webapp/Dockerfile
new file mode 100644
index 000000000..9b7f1329c
--- /dev/null
+++ b/Human-Connection/webapp/Dockerfile
@@ -0,0 +1,27 @@
+FROM node:12.5-alpine as base
+LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
+
+EXPOSE 3000
+CMD ["yarn", "run", "start"]
+
+# Expose the app port
+ARG BUILD_COMMIT
+ENV BUILD_COMMIT=$BUILD_COMMIT
+ARG WORKDIR=/nitro-web
+RUN mkdir -p $WORKDIR
+WORKDIR $WORKDIR
+
+# See: https://github.com/nodejs/docker-node/pull/367#issuecomment-430807898
+RUN apk --no-cache add git
+
+COPY . .
+
+FROM base as build-and-test
+RUN cp .env.template .env
+RUN yarn install --ignore-engines --production=false --frozen-lockfile --non-interactive
+RUN yarn run build
+
+FROM base as production
+ENV NODE_ENV=production
+COPY --from=build-and-test ./nitro-web/node_modules ./node_modules
+COPY --from=build-and-test ./nitro-web/.nuxt ./.nuxt
diff --git a/Human-Connection/webapp/README.md b/Human-Connection/webapp/README.md
new file mode 100644
index 000000000..ce27eca2f
--- /dev/null
+++ b/Human-Connection/webapp/README.md
@@ -0,0 +1,44 @@
+# Webapp
+
+
+
+## Installation
+
+```bash
+# install all dependencies
+$ yarn install
+```
+
+Copy:
+
+```text
+cp .env.template .env
+cp cypress.env.template.json cypress.env.json
+```
+
+Configure the files according to your needs and your local setup.
+
+### Build for Development
+
+```bash
+# serve with hot reload at localhost:3000
+$ yarn dev
+```
+
+### Build for Production
+
+```bash
+# build for production and launch server
+$ yarn build
+$ yarn start
+```
+
+## Styleguide
+
+All reusable Components \(for example avatar\) should be done inside the [Nitro-Styleguide](https://github.com/Human-Connection/Nitro-Styleguide) repository.
+
+
+
+More information can be found here: [https://github.com/Human-Connection/Nitro-Styleguide](https://github.com/Human-Connection/Nitro-Styleguide)
+
+If you need to change something in the styleguide and want to see the effects on the frontend immediately, then we have you covered. You need to clone the styleguide to the parent directory `../Nitro-Styleguide` and run `yarn && yarn run dev`. After that you run `yarn run dev:styleguide` instead of `yarn run dev` and you will see your changes reflected inside the fronten!
diff --git a/Human-Connection/webapp/assets.md b/Human-Connection/webapp/assets.md
new file mode 100644
index 000000000..06786539d
--- /dev/null
+++ b/Human-Connection/webapp/assets.md
@@ -0,0 +1,8 @@
+# ASSETS
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains your un-compiled assets such as LESS, SASS, or JavaScript.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).
+
diff --git a/Human-Connection/webapp/assets/styles/imports/_toast.scss b/Human-Connection/webapp/assets/styles/imports/_toast.scss
new file mode 100644
index 000000000..0ef81a5b2
--- /dev/null
+++ b/Human-Connection/webapp/assets/styles/imports/_toast.scss
@@ -0,0 +1,32 @@
+.iziToast-target, .iziToast {
+ &,
+ &:after,
+ &.iziToast-color-dark:after {
+ box-shadow: none !important;
+ }
+}
+
+.iziToast .iziToast-message {
+ font-weight: 400 !important;
+}
+
+.iziToast.iziToast-color-red {
+ background: $color-danger !important;
+ border-color: $color-danger !important;
+}
+.iziToast.iziToast-color-orange {
+ background: $color-warning !important;
+ border-color: $color-warning !important;
+}
+.iziToast.iziToast-color-yellow {
+ background: $color-yellow !important;
+ border-color: $color-yellow !important;
+}
+.iziToast.iziToast-color-blue {
+ background: $color-secondary !important;
+ border-color: $color-secondary !important;
+}
+.iziToast.iziToast-color-green {
+ background: $color-success !important;
+ border-color: $color-success !important;
+}
diff --git a/Human-Connection/webapp/assets/styles/imports/_tooltip.scss b/Human-Connection/webapp/assets/styles/imports/_tooltip.scss
new file mode 100644
index 000000000..e3032cb56
--- /dev/null
+++ b/Human-Connection/webapp/assets/styles/imports/_tooltip.scss
@@ -0,0 +1,127 @@
+@mixin arrow($size, $type, $color) {
+
+ --#{$type}-arrow-size: $size;
+
+ .#{$type}-arrow {
+ width: 0;
+ height: 0;
+ border-style: solid;
+ position: absolute;
+ margin: $size;
+ border-color: $color;
+ z-index: 1;
+ }
+
+ &[x-placement^="top"] {
+ margin-bottom: $size;
+
+ .#{$type}-arrow {
+ border-width: $size $size 0 $size;
+ border-left-color: transparent !important;
+ border-right-color: transparent !important;
+ border-bottom-color: transparent !important;
+ bottom: -$size;
+ left: calc(50% - var(--#{$type}-arrow-size));
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+ }
+
+ &[x-placement^="bottom"] {
+ margin-top: $size;
+
+ .#{$type}-arrow {
+ border-width: 0 $size $size $size;
+ border-left-color: transparent !important;
+ border-right-color: transparent !important;
+ border-top-color: transparent !important;
+ top: -$size;
+ left: calc(50% - var(--#{$type}-arrow-size));
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+ }
+
+ &[x-placement^="right"] {
+ margin-left: $size;
+
+ .#{$type}-arrow {
+ border-width: $size $size $size 0;
+ border-left-color: transparent !important;
+ border-top-color: transparent !important;
+ border-bottom-color: transparent !important;
+ left: -$size;
+ top: calc(50% - var(--#{$type}-arrow-size));
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
+
+ &[x-placement^="left"] {
+ margin-right: $size;
+
+ .#{$type}-arrow {
+ border-width: $size 0 $size $size;
+ border-top-color: transparent !important;
+ border-right-color: transparent !important;
+ border-bottom-color: transparent !important;
+ right: -$size;
+ top: calc(50% - var(--#{$type}-arrow-size));
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
+}
+
+.tooltip {
+ display: block !important;
+ z-index: $z-index-modal - 2;
+
+ .tooltip-inner {
+ background: $background-color-inverse-soft;
+ color: $text-color-inverse;
+ border-radius: $border-radius-base;
+ padding: $space-x-small $space-small;
+ box-shadow: $box-shadow-large;
+ }
+
+ @include arrow(5px, "tooltip", $background-color-inverse-soft);
+
+ &.popover {
+ .popover-inner {
+ background: $background-color-soft;
+ color: $text-color-base;
+ border-radius: $border-radius-base;
+ padding: $space-x-small $space-small;
+ box-shadow: $box-shadow-x-large;
+
+ nav {
+ margin-left: -$space-small;
+ margin-right: -$space-small;
+
+ a {
+ padding-left: 12px;
+ }
+ }
+ }
+
+ .popover-arrow {
+ border-color: $background-color-soft;
+ }
+
+ @include arrow(7px, "popover", $background-color-soft);
+ }
+
+
+ &[aria-hidden='true'] {
+ visibility: hidden;
+ opacity: 0;
+ transition: opacity 60ms;
+ }
+
+ &[aria-hidden='false'] {
+ visibility: visible;
+ opacity: 1;
+ transition: opacity 60ms;
+ }
+}
diff --git a/Human-Connection/webapp/assets/styles/main.scss b/Human-Connection/webapp/assets/styles/main.scss
new file mode 100644
index 000000000..560249b4a
--- /dev/null
+++ b/Human-Connection/webapp/assets/styles/main.scss
@@ -0,0 +1,165 @@
+@import './imports/_tooltip.scss';
+@import './imports/_toast.scss';
+
+// Transition Easing
+$easeOut: cubic-bezier(0.19, 1, 0.22, 1);
+
+.disabled-content {
+ position: relative;
+
+ &::before {
+ @include border-radius($border-radius-x-large);
+ box-shadow: inset 0 0 0 5px $color-danger;
+ content: '';
+ display: block;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: 2;
+ pointer-events: none;
+ }
+}
+
+.layout-enter-active {
+ transition: opacity 80ms ease-out;
+ transition-delay: 80ms;
+}
+.layout-leave-active {
+ transition: opacity 80ms ease-in;
+}
+.layout-enter,
+.layout-leave-active {
+ opacity: 0;
+}
+
+// slide up ease
+.slide-up-enter-active {
+ transition: all 500ms $easeOut;
+ transition-delay: 20ms;
+ opacity: 1;
+ transform: translate3d(0, 0, 0);
+}
+.slide-up-enter,
+.slide-up-leave-active {
+ opacity: 0;
+ box-shadow: none;
+ transform: translate3d(0, 15px, 0);
+}
+
+.main-navigation {
+ background: #fff;
+}
+
+blockquote {
+ display: block;
+ padding: 15px 20px 15px 45px;
+ margin: 0 0 20px;
+ position: relative;
+
+ /*Font*/
+ font-size: $font-size-base;
+ line-height: 1.2;
+ color: $color-neutral-40;
+ font-family: $font-family-serif;
+ font-style: italic;
+
+ border-left: 3px dotted $color-neutral-70;
+
+ &::before {
+ content: '\201C'; /*Unicode for Left Double Quote*/
+
+ /*Font*/
+ font-size: $font-size-xxxx-large;
+ font-weight: bold;
+ color: $color-neutral-50;
+
+ /*Positioning*/
+ position: absolute;
+ left: 10px;
+ top: 5px;
+ }
+
+ p {
+ margin-top: 0;
+ }
+}
+.main-navigation {
+ box-shadow: $box-shadow-base;
+ position: fixed;
+ width: 100%;
+ z-index: 10;
+
+ a {
+ outline: none;
+ }
+}
+
+hr {
+ border: 0;
+ width: 100%;
+ color: $color-neutral-80;
+ background-color: $color-neutral-80;
+ height: 1px !important;
+}
+
+[class$='menu-trigger'] {
+ user-select: none;
+}
+[class$='menu-popover'] {
+ display: inline-block;
+
+ nav {
+ margin-left: -17px;
+ margin-right: -15px;
+ }
+}
+
+#overlay {
+ display: block;
+ opacity: 0;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ position: fixed;
+ background: rgba(0, 0, 0, 0.15);
+ z-index: 99;
+ pointer-events: none;
+ transition: opacity 150ms ease-out;
+ transition-delay: 50ms;
+
+ .dropdown-open & {
+ opacity: 1;
+ transition-delay: 0;
+ transition: opacity 80ms ease-out;
+ }
+}
+
+.ds-card .ds-section {
+ padding: 0;
+ margin-left: -$space-base;
+ margin-right: -$space-base;
+
+ .ds-container {
+ padding: $space-base;
+ }
+}
+
+[class$='menu-popover'] {
+ min-width: 130px;
+
+ a,
+ button {
+ display: flex;
+ align-content: center;
+ align-items: center;
+
+ .ds-icon {
+ padding-right: $space-xx-small;
+ }
+ }
+}
+
+.v-popover.open .trigger a {
+ color: $text-color-link-active;
+}
diff --git a/Human-Connection/webapp/components.md b/Human-Connection/webapp/components.md
new file mode 100644
index 000000000..be43ae454
--- /dev/null
+++ b/Human-Connection/webapp/components.md
@@ -0,0 +1,8 @@
+# COMPONENTS
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+The components directory contains your Vue.js Components.
+
+_Nuxt.js doesn't supercharge these components._
+
diff --git a/Human-Connection/webapp/components/Avatar/Avatar.spec.js b/Human-Connection/webapp/components/Avatar/Avatar.spec.js
new file mode 100644
index 000000000..626e584c9
--- /dev/null
+++ b/Human-Connection/webapp/components/Avatar/Avatar.spec.js
@@ -0,0 +1,71 @@
+import { mount, createLocalVue } from '@vue/test-utils'
+import Styleguide from '@human-connection/styleguide'
+import Avatar from './Avatar.vue'
+import Filters from '~/plugins/vue-filters'
+
+const localVue = createLocalVue()
+localVue.use(Styleguide)
+localVue.use(Filters)
+
+describe('Avatar.vue', () => {
+ let propsData = {}
+
+ const Wrapper = () => {
+ return mount(Avatar, { propsData, localVue })
+ }
+
+ it('renders no image', () => {
+ expect(
+ Wrapper()
+ .find('img')
+ .exists(),
+ ).toBe(false)
+ })
+
+ it('renders an icon', () => {
+ expect(
+ Wrapper()
+ .find('.ds-icon')
+ .exists(),
+ ).toBe(true)
+ })
+
+ describe('given a user', () => {
+ describe('with a relative avatar url', () => {
+ beforeEach(() => {
+ propsData = {
+ user: {
+ avatar: '/avatar.jpg',
+ },
+ }
+ })
+
+ it('adds a prefix to load the image from the uploads service', () => {
+ expect(
+ Wrapper()
+ .find('img')
+ .attributes('src'),
+ ).toBe('/api/avatar.jpg')
+ })
+ })
+
+ describe('with an absolute avatar url', () => {
+ beforeEach(() => {
+ propsData = {
+ user: {
+ avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
+ },
+ }
+ })
+
+ it('keeps the avatar URL as is', () => {
+ // e.g. our seeds have absolute image URLs
+ expect(
+ Wrapper()
+ .find('img')
+ .attributes('src'),
+ ).toBe('https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg')
+ })
+ })
+ })
+})
diff --git a/Human-Connection/webapp/components/Avatar/Avatar.vue b/Human-Connection/webapp/components/Avatar/Avatar.vue
new file mode 100644
index 000000000..ec2f9b28b
--- /dev/null
+++ b/Human-Connection/webapp/components/Avatar/Avatar.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/Human-Connection/webapp/components/Badges.spec.js b/Human-Connection/webapp/components/Badges.spec.js
new file mode 100644
index 000000000..5273fca21
--- /dev/null
+++ b/Human-Connection/webapp/components/Badges.spec.js
@@ -0,0 +1,21 @@
+import { shallowMount } from '@vue/test-utils'
+import Badges from './Badges.vue'
+
+describe('Badges.vue', () => {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallowMount(Badges, {})
+ })
+
+ it('renders', () => {
+ expect(wrapper.is('div')).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
+})
diff --git a/Human-Connection/webapp/components/Badges.vue b/Human-Connection/webapp/components/Badges.vue
new file mode 100644
index 000000000..42ac23e4d
--- /dev/null
+++ b/Human-Connection/webapp/components/Badges.vue
@@ -0,0 +1,68 @@
+
+
+
+
![]()
+
+
+
+
+
+
+
diff --git a/Human-Connection/webapp/components/CategoriesSelect/CategoriesSelect.spec.js b/Human-Connection/webapp/components/CategoriesSelect/CategoriesSelect.spec.js
new file mode 100644
index 000000000..199dacb74
--- /dev/null
+++ b/Human-Connection/webapp/components/CategoriesSelect/CategoriesSelect.spec.js
@@ -0,0 +1,108 @@
+import { mount, createLocalVue } from '@vue/test-utils'
+import CategoriesSelect from './CategoriesSelect'
+import Styleguide from '@human-connection/styleguide'
+
+const localVue = createLocalVue()
+localVue.use(Styleguide)
+
+describe('CategoriesSelect.vue', () => {
+ let wrapper
+ let mocks
+ let democracyAndPolitics
+ let environmentAndNature
+ let consumptionAndSustainablity
+
+ const categories = [
+ {
+ id: 'cat9',
+ name: 'Democracy & Politics',
+ icon: 'university',
+ },
+ {
+ id: 'cat4',
+ name: 'Environment & Nature',
+ icon: 'tree',
+ },
+ {
+ id: 'cat15',
+ name: 'Consumption & Sustainability',
+ icon: 'shopping-cart',
+ },
+ {
+ name: 'Cooperation & Development',
+ icon: 'users',
+ id: 'cat8',
+ },
+ ]
+ beforeEach(() => {
+ mocks = {
+ $t: jest.fn(),
+ }
+ })
+
+ describe('shallowMount', () => {
+ const Wrapper = () => {
+ return mount(CategoriesSelect, { mocks, localVue })
+ }
+
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ describe('toggleCategory', () => {
+ beforeEach(() => {
+ wrapper.vm.categories = categories
+ democracyAndPolitics = wrapper.findAll('button').at(0)
+ democracyAndPolitics.trigger('click')
+ })
+
+ it('adds categories to selectedCategoryIds when clicked', () => {
+ expect(wrapper.vm.selectedCategoryIds).toEqual([categories[0].id])
+ })
+
+ it('emits an updateCategories event when the selectedCategoryIds changes', () => {
+ expect(wrapper.emitted().updateCategories[0][0]).toEqual([categories[0].id])
+ })
+
+ it('removes categories when clicked a second time', () => {
+ democracyAndPolitics.trigger('click')
+ expect(wrapper.vm.selectedCategoryIds).toEqual([])
+ })
+
+ it('changes the selectedCount when selectedCategoryIds is updated', () => {
+ expect(wrapper.vm.selectedCount).toEqual(1)
+ democracyAndPolitics.trigger('click')
+ expect(wrapper.vm.selectedCount).toEqual(0)
+ })
+
+ it('sets a category to active when it has been selected', () => {
+ expect(wrapper.vm.isActive(categories[0].id)).toEqual(true)
+ })
+
+ describe('maximum', () => {
+ beforeEach(() => {
+ environmentAndNature = wrapper.findAll('button').at(1)
+ consumptionAndSustainablity = wrapper.findAll('button').at(2)
+ environmentAndNature.trigger('click')
+ consumptionAndSustainablity.trigger('click')
+ })
+
+ it('allows three categories to be selected', () => {
+ expect(wrapper.vm.selectedCategoryIds).toEqual([
+ categories[0].id,
+ categories[1].id,
+ categories[2].id,
+ ])
+ })
+
+ it('sets reachedMaximum to true after three', () => {
+ expect(wrapper.vm.reachedMaximum).toEqual(true)
+ })
+
+ it('sets other categories to disabled after three', () => {
+ expect(wrapper.vm.isDisabled(categories[3].id)).toEqual(true)
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/webapp/components/CategoriesSelect/CategoriesSelect.vue b/Human-Connection/webapp/components/CategoriesSelect/CategoriesSelect.vue
new file mode 100644
index 000000000..163f31419
--- /dev/null
+++ b/Human-Connection/webapp/components/CategoriesSelect/CategoriesSelect.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+ {{ category.name }}
+
+
+
+
+
+ {{
+ $t('contribution.categories.infoSelectedNoOfMaxCategories', {
+ chosen: selectedCount,
+ max: selectedMax,
+ })
+ }}
+
+
+
+
+
diff --git a/Human-Connection/webapp/components/Category/Readme.md b/Human-Connection/webapp/components/Category/Readme.md
new file mode 100644
index 000000000..50e07f966
--- /dev/null
+++ b/Human-Connection/webapp/components/Category/Readme.md
@@ -0,0 +1,7 @@
+### Example
+
+Category "IT, Internet & Data Privacy" with icon "mouse-cursor"
+
+```
+
+```
\ No newline at end of file
diff --git a/Human-Connection/webapp/components/Category/index.spec.js b/Human-Connection/webapp/components/Category/index.spec.js
new file mode 100644
index 000000000..7ce0b7243
--- /dev/null
+++ b/Human-Connection/webapp/components/Category/index.spec.js
@@ -0,0 +1,35 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils'
+import Styleguide from '@human-connection/styleguide'
+import Category from './index'
+
+const localVue = createLocalVue()
+localVue.use(Styleguide)
+
+describe('Category', () => {
+ let icon
+ let name
+
+ let Wrapper = () => {
+ return shallowMount(Category, {
+ localVue,
+ propsData: {
+ icon,
+ name,
+ },
+ })
+ }
+
+ describe('given Strings for Icon and Name', () => {
+ beforeEach(() => {
+ icon = 'mouse-cursor'
+ name = 'Peter'
+ })
+
+ it('shows Name', () => {
+ expect(Wrapper().text()).toContain('Peter')
+ })
+ it('shows Icon Svg', () => {
+ expect(Wrapper().contains('svg'))
+ })
+ })
+})
diff --git a/Human-Connection/webapp/components/Category/index.vue b/Human-Connection/webapp/components/Category/index.vue
new file mode 100644
index 000000000..acc35772a
--- /dev/null
+++ b/Human-Connection/webapp/components/Category/index.vue
@@ -0,0 +1,16 @@
+
+
+
+ {{ name }}
+
+
+
+
diff --git a/Human-Connection/webapp/components/Comment.spec.js b/Human-Connection/webapp/components/Comment.spec.js
new file mode 100644
index 000000000..4fdc48bbd
--- /dev/null
+++ b/Human-Connection/webapp/components/Comment.spec.js
@@ -0,0 +1,139 @@
+import { config, shallowMount, createLocalVue } from '@vue/test-utils'
+import Comment from './Comment.vue'
+import Vuex from 'vuex'
+import Styleguide from '@human-connection/styleguide'
+
+const localVue = createLocalVue()
+
+localVue.use(Vuex)
+localVue.use(Styleguide)
+
+config.stubs['no-ssr'] = ''
+
+describe('Comment.vue', () => {
+ let propsData
+ let mocks
+ let getters
+ let wrapper
+ let Wrapper
+
+ beforeEach(() => {
+ propsData = {}
+ mocks = {
+ $t: jest.fn(),
+ $toast: {
+ success: jest.fn(),
+ error: jest.fn(),
+ },
+ $filters: {
+ truncate: a => a,
+ },
+ $apollo: {
+ mutate: jest.fn().mockResolvedValue(),
+ },
+ }
+ getters = {
+ 'auth/user': () => {
+ return {}
+ },
+ 'auth/isModerator': () => false,
+ }
+ })
+
+ describe('shallowMount', () => {
+ Wrapper = () => {
+ const store = new Vuex.Store({
+ getters,
+ })
+ return shallowMount(Comment, {
+ store,
+ propsData,
+ mocks,
+ localVue,
+ })
+ }
+
+ describe('given a comment', () => {
+ beforeEach(() => {
+ propsData.comment = {
+ id: '2',
+ contentExcerpt: 'Hello I am a comment content',
+ }
+ })
+
+ it('renders content', () => {
+ wrapper = Wrapper()
+ expect(wrapper.text()).toMatch('Hello I am a comment content')
+ })
+
+ describe('which is disabled', () => {
+ beforeEach(() => {
+ propsData.comment.disabled = true
+ })
+
+ it('renders no comment data', () => {
+ wrapper = Wrapper()
+ expect(wrapper.text()).not.toMatch('comment content')
+ })
+
+ it('has no "disabled-content" css class', () => {
+ wrapper = Wrapper()
+ expect(wrapper.classes()).not.toContain('disabled-content')
+ })
+
+ it('translates a placeholder', () => {
+ wrapper = Wrapper()
+ const calls = mocks.$t.mock.calls
+ const expected = [['comment.content.unavailable-placeholder']]
+ expect(calls).toEqual(expect.arrayContaining(expected))
+ })
+
+ describe('for a moderator', () => {
+ beforeEach(() => {
+ getters['auth/isModerator'] = () => true
+ })
+
+ it('renders comment data', () => {
+ wrapper = Wrapper()
+ expect(wrapper.text()).toMatch('comment content')
+ })
+
+ it('has a "disabled-content" css class', () => {
+ wrapper = Wrapper()
+ expect(wrapper.classes()).toContain('disabled-content')
+ })
+ })
+ })
+
+ beforeEach(jest.useFakeTimers)
+
+ describe('test callbacks', () => {
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ describe('deletion of Comment from List by invoking "deleteCommentCallback()"', () => {
+ beforeEach(() => {
+ wrapper.vm.deleteCommentCallback()
+ })
+
+ describe('after timeout', () => {
+ beforeEach(jest.runAllTimers)
+
+ it('emits "deleteComment"', () => {
+ expect(wrapper.emitted().deleteComment.length).toBe(1)
+ })
+
+ it('does call mutation', () => {
+ expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
+ })
+
+ it('mutation is successful', () => {
+ expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
+ })
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/Human-Connection/webapp/components/Comment.vue b/Human-Connection/webapp/components/Comment.vue
new file mode 100644
index 000000000..d8f63526f
--- /dev/null
+++ b/Human-Connection/webapp/components/Comment.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+ {{ this.$t('comment.content.unavailable-placeholder') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Human-Connection/webapp/components/ContentMenu.vue b/Human-Connection/webapp/components/ContentMenu.vue
new file mode 100644
index 000000000..4a1c2ed19
--- /dev/null
+++ b/Human-Connection/webapp/components/ContentMenu.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
diff --git a/Human-Connection/webapp/components/ContributionForm/ContributionForm.spec.js b/Human-Connection/webapp/components/ContributionForm/ContributionForm.spec.js
new file mode 100644
index 000000000..0813d16f0
--- /dev/null
+++ b/Human-Connection/webapp/components/ContributionForm/ContributionForm.spec.js
@@ -0,0 +1,258 @@
+import { config, mount, createLocalVue } from '@vue/test-utils'
+import ContributionForm from './ContributionForm.vue'
+import Styleguide from '@human-connection/styleguide'
+import Vuex from 'vuex'
+import PostMutations from '~/graphql/PostMutations.js'
+import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
+import Filters from '~/plugins/vue-filters'
+import TeaserImage from '~/components/TeaserImage/TeaserImage'
+
+const localVue = createLocalVue()
+
+localVue.use(Vuex)
+localVue.use(Styleguide)
+localVue.use(Filters)
+
+config.stubs['no-ssr'] = ''
+
+describe('ContributionForm.vue', () => {
+ let wrapper
+ let postTitleInput
+ let expectedParams
+ let deutschOption
+ let cancelBtn
+ let mocks
+ let propsData
+ const postTitle = 'this is a title for a post'
+ const postContent = 'this is a post'
+ const imageUpload = {
+ file: { filename: 'avataar.svg', previewElement: '' },
+ url: 'someUrlToImage',
+ }
+ const image = '/uploads/1562010976466-avataaars'
+ beforeEach(() => {
+ mocks = {
+ $t: jest.fn(),
+ $apollo: {
+ mutate: jest
+ .fn()
+ .mockResolvedValueOnce({
+ data: {
+ CreatePost: {
+ title: postTitle,
+ slug: 'this-is-a-title-for-a-post',
+ content: postContent,
+ contentExcerpt: postContent,
+ language: 'en',
+ },
+ },
+ })
+ .mockRejectedValue({ message: 'Not Authorised!' }),
+ },
+ $toast: {
+ error: jest.fn(),
+ success: jest.fn(),
+ },
+ $i18n: {
+ locale: () => 'en',
+ },
+ $router: {
+ back: jest.fn(),
+ push: jest.fn(),
+ },
+ }
+ propsData = {}
+ })
+
+ describe('mount', () => {
+ const getters = {
+ 'editor/placeholder': () => {
+ return 'some cool placeholder'
+ },
+ }
+ const store = new Vuex.Store({
+ getters,
+ })
+ const Wrapper = () => {
+ return mount(ContributionForm, { mocks, localVue, store, propsData })
+ }
+
+ beforeEach(() => {
+ wrapper = Wrapper()
+ wrapper.setData({ form: { languageOptions: [{ label: 'Deutsch', value: 'de' }] } })
+ })
+
+ describe('CreatePost', () => {
+ describe('language placeholder', () => {
+ it("displays the name that corresponds with the user's location code", () => {
+ expect(wrapper.find('.ds-select-placeholder').text()).toEqual('English')
+ })
+ })
+
+ describe('invalid form submission', () => {
+ it('title required for form submission', async () => {
+ postTitleInput = wrapper.find('.ds-input')
+ postTitleInput.setValue(postTitle)
+ await wrapper.find('form').trigger('submit')
+ expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
+ })
+
+ it('content required for form submission', async () => {
+ wrapper.vm.updateEditorContent(postContent)
+ await wrapper.find('form').trigger('submit')
+ expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('valid form submission', () => {
+ beforeEach(async () => {
+ expectedParams = {
+ mutation: PostMutations().CreatePost,
+ variables: {
+ title: postTitle,
+ content: postContent,
+ language: 'en',
+ id: null,
+ categoryIds: null,
+ imageUpload: null,
+ image: null,
+ },
+ }
+ postTitleInput = wrapper.find('.ds-input')
+ postTitleInput.setValue(postTitle)
+ wrapper.vm.updateEditorContent(postContent)
+ await wrapper.find('form').trigger('submit')
+ })
+
+ it('with title and content', () => {
+ expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
+ })
+
+ it("sends a fallback language based on a user's locale", () => {
+ expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
+ })
+
+ it('supports changing the language', async () => {
+ expectedParams.variables.language = 'de'
+ deutschOption = wrapper.findAll('li').at(0)
+ deutschOption.trigger('click')
+ await wrapper.find('form').trigger('submit')
+ expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
+ })
+
+ it('supports adding categories', async () => {
+ const categoryIds = ['cat12', 'cat15', 'cat37']
+ expectedParams.variables.categoryIds = categoryIds
+ wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
+ await wrapper.find('form').trigger('submit')
+ expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
+ })
+
+ it('supports adding a teaser image', async () => {
+ expectedParams.variables.imageUpload = imageUpload
+ wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload)
+ await wrapper.find('form').trigger('submit')
+ expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
+ })
+
+ it("pushes the user to the post's page", async () => {
+ expect(mocks.$router.push).toHaveBeenCalledTimes(1)
+ })
+
+ it('shows a success toaster', () => {
+ expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ describe('cancel', () => {
+ it('calls $router.back() when cancel button clicked', () => {
+ cancelBtn = wrapper.find('.cancel-button')
+ cancelBtn.trigger('click')
+ expect(mocks.$router.back).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ describe('handles errors', () => {
+ beforeEach(async () => {
+ jest.useFakeTimers()
+ wrapper = Wrapper()
+ postTitleInput = wrapper.find('.ds-input')
+ postTitleInput.setValue(postTitle)
+ wrapper.vm.updateEditorContent(postContent)
+ // second submission causes mutation to reject
+ await wrapper.find('form').trigger('submit')
+ })
+
+ it('shows an error toaster when apollo mutation rejects', async () => {
+ await wrapper.find('form').trigger('submit')
+ await mocks.$apollo.mutate
+ expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorised!')
+ })
+ })
+ })
+
+ describe('UpdatePost', () => {
+ beforeEach(() => {
+ propsData = {
+ contribution: {
+ id: 'p1456',
+ slug: 'dies-ist-ein-post',
+ title: 'dies ist ein Post',
+ content: 'auf Deutsch geschrieben',
+ language: 'de',
+ image,
+ categories: [{ id: 'cat12', name: 'Democracy & Politics' }],
+ },
+ }
+ wrapper = Wrapper()
+ })
+
+ it('sets id equal to contribution id', () => {
+ expect(wrapper.vm.id).toEqual(propsData.contribution.id)
+ })
+
+ it('sets slug equal to contribution slug', () => {
+ expect(wrapper.vm.slug).toEqual(propsData.contribution.slug)
+ })
+
+ it('sets title equal to contribution title', () => {
+ expect(wrapper.vm.form.title).toEqual(propsData.contribution.title)
+ })
+
+ it('sets content equal to contribution content', () => {
+ expect(wrapper.vm.form.content).toEqual(propsData.contribution.content)
+ })
+
+ it('calls the UpdatePost apollo mutation', async () => {
+ expectedParams = {
+ mutation: PostMutations().UpdatePost,
+ variables: {
+ title: postTitle,
+ content: postContent,
+ language: propsData.contribution.language,
+ id: propsData.contribution.id,
+ categoryIds: ['cat12'],
+ image,
+ imageUpload: null,
+ },
+ }
+ postTitleInput = wrapper.find('.ds-input')
+ postTitleInput.setValue(postTitle)
+ wrapper.vm.updateEditorContent(postContent)
+ await wrapper.find('form').trigger('submit')
+ expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
+ })
+
+ it('supports updating categories', async () => {
+ const categoryIds = ['cat3', 'cat51', 'cat37']
+ postTitleInput = wrapper.find('.ds-input')
+ postTitleInput.setValue(postTitle)
+ wrapper.vm.updateEditorContent(postContent)
+ expectedParams.variables.categoryIds = categoryIds
+ wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
+ await wrapper.find('form').trigger('submit')
+ expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
+ })
+ })
+ })
+})
diff --git a/Human-Connection/webapp/components/ContributionForm/ContributionForm.vue b/Human-Connection/webapp/components/ContributionForm/ContributionForm.vue
new file mode 100644
index 000000000..c6bb2cdc4
--- /dev/null
+++ b/Human-Connection/webapp/components/ContributionForm/ContributionForm.vue
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('actions.cancel') }}
+
+
+ {{ $t('actions.save') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Human-Connection/webapp/components/CountTo.vue b/Human-Connection/webapp/components/CountTo.vue
new file mode 100644
index 000000000..ee0e3e082
--- /dev/null
+++ b/Human-Connection/webapp/components/CountTo.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
diff --git a/Human-Connection/webapp/components/DeleteData/DeleteData.spec.js b/Human-Connection/webapp/components/DeleteData/DeleteData.spec.js
new file mode 100644
index 000000000..139316ed2
--- /dev/null
+++ b/Human-Connection/webapp/components/DeleteData/DeleteData.spec.js
@@ -0,0 +1,183 @@
+import { mount, createLocalVue } from '@vue/test-utils'
+import DeleteData from './DeleteData.vue'
+import Styleguide from '@human-connection/styleguide'
+import Vuex from 'vuex'
+
+const localVue = createLocalVue()
+
+localVue.use(Vuex)
+localVue.use(Styleguide)
+
+describe('DeleteData.vue', () => {
+ let mocks
+ let wrapper
+ let getters
+ let actions
+ let deleteAccountBtn
+ let enableDeletionInput
+ let enableContributionDeletionCheckbox
+ let enableCommentDeletionCheckbox
+ const deleteAccountName = 'Delete MyAccount'
+ const deleteContributionsMessage = 'Delete my 2 posts'
+ const deleteCommentsMessage = 'Delete my 3 comments'
+
+ beforeEach(() => {
+ mocks = {
+ $t: jest.fn(),
+ $apollo: {
+ mutate: jest
+ .fn()
+ .mockResolvedValueOnce({
+ data: {
+ DeleteData: {
+ id: 'u343',
+ },
+ },
+ })
+ .mockRejectedValue({ message: 'Not authorised!' }),
+ },
+ $toast: {
+ error: jest.fn(),
+ success: jest.fn(),
+ },
+ $router: {
+ history: {
+ push: jest.fn(),
+ },
+ },
+ }
+ getters = {
+ 'auth/user': () => {
+ return { id: 'u343', name: deleteAccountName, contributionsCount: 2, commentsCount: 3 }
+ },
+ }
+ actions = { 'auth/logout': jest.fn() }
+ })
+
+ describe('mount', () => {
+ const Wrapper = () => {
+ const store = new Vuex.Store({
+ getters,
+ actions,
+ })
+ return mount(DeleteData, { mocks, localVue, store })
+ }
+
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it('defaults to deleteContributions to false', () => {
+ expect(wrapper.vm.deleteContributions).toEqual(false)
+ })
+
+ it('defaults to deleteComments to false', () => {
+ expect(wrapper.vm.deleteComments).toEqual(false)
+ })
+
+ it('defaults to deleteEnabled to false', () => {
+ expect(wrapper.vm.deleteEnabled).toEqual(false)
+ })
+
+ it('does not call the delete user mutation if deleteEnabled is false', () => {
+ deleteAccountBtn = wrapper.find('.ds-button-danger')
+ deleteAccountBtn.trigger('click')
+ expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
+ })
+
+ describe('calls the delete user mutation', () => {
+ beforeEach(() => {
+ enableDeletionInput = wrapper.find('.enable-deletion-input input')
+ enableDeletionInput.setValue(deleteAccountName)
+ deleteAccountBtn = wrapper.find('.ds-button-danger')
+ })
+
+ it('if deleteEnabled is true and only deletes user by default', () => {
+ deleteAccountBtn.trigger('click')
+ expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ id: 'u343',
+ resource: [],
+ },
+ }),
+ )
+ })
+
+ it("deletes a user's posts if requested", () => {
+ mocks.$t.mockImplementation(() => deleteContributionsMessage)
+ enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
+ enableContributionDeletionCheckbox.trigger('click')
+ deleteAccountBtn.trigger('click')
+ expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ id: 'u343',
+ resource: ['Post'],
+ },
+ }),
+ )
+ })
+
+ it("deletes a user's comments if requested", () => {
+ mocks.$t.mockImplementation(() => deleteCommentsMessage)
+ enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
+ enableCommentDeletionCheckbox.trigger('click')
+ deleteAccountBtn.trigger('click')
+ expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ id: 'u343',
+ resource: ['Comment'],
+ },
+ }),
+ )
+ })
+
+ it("deletes a user's posts and comments if requested", () => {
+ mocks.$t.mockImplementation(() => deleteContributionsMessage)
+ enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
+ enableContributionDeletionCheckbox.trigger('click')
+ mocks.$t.mockImplementation(() => deleteCommentsMessage)
+ enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
+ enableCommentDeletionCheckbox.trigger('click')
+ deleteAccountBtn.trigger('click')
+ expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ id: 'u343',
+ resource: ['Post', 'Comment'],
+ },
+ }),
+ )
+ })
+
+ it('shows a success toaster after successful mutation', async () => {
+ await deleteAccountBtn.trigger('click')
+ expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
+ })
+
+ it('redirect the user to the homepage', async () => {
+ await deleteAccountBtn.trigger('click')
+ expect(mocks.$router.history.push).toHaveBeenCalledWith('/')
+ })
+ })
+
+ describe('error handling', () => {
+ it('shows an error toaster when the mutation rejects', async () => {
+ enableDeletionInput = wrapper.find('.enable-deletion-input input')
+ enableDeletionInput.setValue(deleteAccountName)
+ deleteAccountBtn = wrapper.find('.ds-button-danger')
+ await deleteAccountBtn.trigger('click')
+ // second submission causes mutation to reject
+ await deleteAccountBtn.trigger('click')
+ await mocks.$apollo.mutate
+ expect(mocks.$toast.error).toHaveBeenCalledWith('Not authorised!')
+ })
+ })
+ })
+})
diff --git a/Human-Connection/webapp/components/DeleteData/DeleteData.vue b/Human-Connection/webapp/components/DeleteData/DeleteData.vue
new file mode 100644
index 000000000..293e65221
--- /dev/null
+++ b/Human-Connection/webapp/components/DeleteData/DeleteData.vue
@@ -0,0 +1,225 @@
+
+
+
+
+
+
+
+
+
+
+ {{ $t('settings.deleteUserAccount.name') }}
+
+
+
+ {{ $t('settings.deleteUserAccount.accountDescription') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Human-Connection/webapp/components/Dropdown.vue b/Human-Connection/webapp/components/Dropdown.vue
new file mode 100644
index 000000000..6743529e8
--- /dev/null
+++ b/Human-Connection/webapp/components/Dropdown.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Human-Connection/webapp/components/Editor/index.vue b/Human-Connection/webapp/components/Editor/index.vue
new file mode 100644
index 000000000..84649f436
--- /dev/null
+++ b/Human-Connection/webapp/components/Editor/index.vue
@@ -0,0 +1,599 @@
+
+
+
+
+
+ @{{ user.slug }}
+
+
+
No users found
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Human-Connection/webapp/components/Editor/nodes/Mention.js b/Human-Connection/webapp/components/Editor/nodes/Mention.js
new file mode 100644
index 000000000..dc34a05ff
--- /dev/null
+++ b/Human-Connection/webapp/components/Editor/nodes/Mention.js
@@ -0,0 +1,27 @@
+import { Mention as TipTapMention } from 'tiptap-extensions'
+
+export default class Mention extends TipTapMention {
+ get schema() {
+ const patchedSchema = super.schema
+
+ patchedSchema.attrs = {
+ url: {},
+ label: {},
+ }
+ patchedSchema.toDOM = node => {
+ return [
+ 'a',
+ {
+ class: this.options.mentionClass,
+ href: node.attrs.url,
+ target: '_blank',
+ },
+ `${this.options.matcher.char}${node.attrs.label}`,
+ ]
+ }
+ patchedSchema.parseDOM = [
+ // this is not implemented
+ ]
+ return patchedSchema
+ }
+}
diff --git a/Human-Connection/webapp/components/Editor/plugins/eventHandler.js b/Human-Connection/webapp/components/Editor/plugins/eventHandler.js
new file mode 100644
index 000000000..c390a066d
--- /dev/null
+++ b/Human-Connection/webapp/components/Editor/plugins/eventHandler.js
@@ -0,0 +1,74 @@
+import { Extension, Plugin } from 'tiptap'
+// import { Slice, Fragment } from 'prosemirror-model'
+
+export default class EventHandler extends Extension {
+ get name() {
+ return 'event_handler'
+ }
+ get plugins() {
+ return [
+ new Plugin({
+ props: {
+ transformPastedText(text) {
+ // console.log('#### transformPastedText', text)
+ return text.trim()
+ },
+ transformPastedHTML(html) {
+ html = html
+ // remove all tags with "space only"
+ .replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '')
+ // remove all iframes
+ .replace(/(