diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 35457d215..9bbd6de90 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -5,7 +5,7 @@ before submitting a new issue. Following one of the issue templates will ensure
Thanks!
-->
-## Issue
+## 💬 Issue
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 58ca6b387..fbf7173fc 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,7 +1,8 @@
---
name: 🐛 Bug report
about: Create a report to help us improve
-
+labels: bug
+title: 🐛 [Bug]
---
## :bug: Bugreport
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index ed30ba7ad..1fba3fa58 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,22 +1,12 @@
---
name: 🚀 Feature request
about: Suggest an idea for this project
-
+labels: feature
+title: 🚀 [Feature]
---
## :rocket: Feature
-
-
-
-### Is your feature request related to a problem? Please describe.
-
-
-
-### Describe the prefered solution and alternatives you've considered
-
-
+
### Design & Layout
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index eb5a923dd..aabbc0f0a 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -1,6 +1,8 @@
---
name: 💬 Question
about: If you need help understanding HumanConnection.
+labels: question
+title: 💬 [Question]
---
+## 🍰 Pullrequest
+
### Issues
-- [X] None
-
-### Checklist
-
-- [X] None
-
-### How2Test
-
-
-- [X] None
+- None
### Todo
diff --git a/.gitignore b/.gitignore
index 07623b965..eb661fd6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
.env
.idea
*.iml
-.vscode
.DS_Store
npm-debug.log*
yarn-debug.log*
diff --git a/.travis.yml b/.travis.yml
index 4d9a4c733..42b427a11 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,12 +17,13 @@ before_install:
install:
- docker-compose -f docker-compose.yml -f docker-compose.travis.yml up --build -d
- - wait-on http://localhost:7474 && docker-compose exec neo4j migrate
+ # avoid "Database constraints have changed after this transaction started"
+ - wait-on http://localhost:7474
script:
# Backend
- docker-compose exec backend yarn run lint
- - docker-compose exec backend yarn run test:jest --ci --verbose=false
+ - 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
@@ -30,7 +31,7 @@ script:
- 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
+ - docker-compose exec webapp yarn run test --ci --verbose=false --coverage
- docker-compose exec -d backend yarn run test:before:seeder
# Fullstack
- CYPRESS_RETRIES=1 yarn run cypress:run
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 000000000..e2d92ff83
--- /dev/null
+++ b/.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/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..908252f41
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,12 @@
+{
+ "eslint.validate": [
+ "javascript",
+ "javascriptreact",
+ {
+ "language": "vue",
+ "autoFix": true
+ }
+ ],
+ "editor.formatOnSave": true,
+ "eslint.autoFixOnSave": true
+}
\ No newline at end of file
diff --git a/SUMMARY.md b/SUMMARY.md
index fdf3600b4..701eac2d0 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -3,6 +3,7 @@
* [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)
diff --git a/backend/.env.template b/backend/.env.template
index abc62b2dc..e905d1eb6 100644
--- a/backend/.env.template
+++ b/backend/.env.template
@@ -4,7 +4,7 @@ NEO4J_PASSWORD=letmein
GRAPHQL_PORT=4000
GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000
-MOCK=false
+MOCKS=false
JWT_SECRET="b/&&7b78BF&fv/Vd"
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"
diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js
index 0fdbfd52d..0000bb066 100644
--- a/backend/.eslintrc.js
+++ b/backend/.eslintrc.js
@@ -1,20 +1,25 @@
module.exports = {
- "extends": "standard",
- "parser": "babel-eslint",
- "env": {
- "es6": true,
- "node": true,
- "jest/globals": true
+ env: {
+ es6: true,
+ node: true,
+ jest: true
},
- "rules": {
- "indent": [
- "error",
- 2
- ],
- "quotes": [
- "error",
- "single"
- ]
+ 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'],
},
- "plugins": ["jest"]
};
diff --git a/backend/.prettierrc.js b/backend/.prettierrc.js
new file mode 100644
index 000000000..e2cf91e91
--- /dev/null
+++ b/backend/.prettierrc.js
@@ -0,0 +1,9 @@
+
+module.exports = {
+ semi: false,
+ printWidth: 100,
+ singleQuote: true,
+ trailingComma: "all",
+ tabWidth: 2,
+ bracketSpacing: true
+};
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 750d284dc..d24f2747e 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:10-alpine as base
+FROM node:12.4-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
@@ -7,6 +7,9 @@ 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"]
diff --git a/backend/README.md b/backend/README.md
index 7c4d3a3e9..3cce123ac 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -1,8 +1,6 @@
# Backend
-## Installation
-{% tabs %}
-{% tab title="Docker" %}
+## Installation with Docker
Run the following command to install everything through docker.
@@ -14,28 +12,15 @@ $ docker-compose up
# rebuild the containers for a cleanup
$ docker-compose up --build
```
-Open another terminal and create unique indices with:
-```bash
-$ docker-compose exec neo4j migrate
-```
+Wait a little until your backend is up and running at [http://localhost:4000/](http://localhost:4000/).
-{% endtab %}
-
-{% tab title="Without Docker" %}
+## Installation without Docker
For the local installation you need a recent version of [node](https://nodejs.org/en/)
-(>= `v10.12.0`) and [Neo4J](https://neo4j.com/) along with
-[Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures) plugin installed
-on your system.
+(>= `v10.12.0`).
-Download [Neo4j Community Edition](https://neo4j.com/download-center/#releases) 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
-Note that grand-stack-starter does not currently bundle a distribution of Neo4j. 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/) 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 `.env`.
-Start Neo4J and confirm the database is running at [http://localhost:7474](http://localhost:7474).
-
-Now install node dependencies with [yarn](https://yarnpkg.com/en/):
+Install node dependencies with [yarn](https://yarnpkg.com/en/):
```bash
$ cd backend
$ yarn install
@@ -46,14 +31,8 @@ Copy Environment Variables:
# in backend/
$ cp .env.template .env
```
-
-Configure the new files according to your needs and your local setup.
-
-Create unique indices with:
-
-```bash
-$ ./neo4j/migrate.sh
-```
+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
@@ -65,17 +44,12 @@ or start the backend in production environment with:
yarn run start
```
-{% endtab %}
-{% endtabs %}
-
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.
+This will start the GraphQL service \(by default on localhost:4000\) where you
+can issue GraphQL requests or access GraphQL Playground in the browser.

-You can access Neo4J through [http://localhost:7474/](http://localhost:7474/)
-for an interactive `cypher` shell and a visualization of the graph.
-
#### Seed Database
@@ -114,7 +88,8 @@ $ yarn run db:reset
# 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!
+**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 %}
diff --git a/backend/package.json b/backend/package.json
index d940937a8..40ea9476d 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -6,8 +6,8 @@
"scripts": {
"build": "babel src/ -d dist/ --copy-files",
"start": "node dist/",
- "dev": "nodemon --exec babel-node src/ -e js,graphql",
- "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,graphql",
+ "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",
@@ -19,14 +19,13 @@
"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": "babel-node src/seed/reset-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,
- "collectCoverage": true,
"collectCoverageFrom": [
"**/*.js",
"!**/node_modules/**",
@@ -44,34 +43,34 @@
},
"dependencies": {
"activitystrea.ms": "~2.1.3",
- "apollo-cache-inmemory": "~1.5.1",
- "apollo-client": "~2.5.1",
+ "apollo-cache-inmemory": "~1.6.2",
+ "apollo-client": "~2.6.2",
"apollo-link-context": "~1.0.14",
"apollo-link-http": "~1.5.14",
- "apollo-server": "~2.5.0",
+ "apollo-server": "~2.6.2",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~5.2.0",
- "date-fns": "2.0.0-alpha.27",
+ "date-fns": "2.0.0-alpha.31",
"debug": "~4.1.1",
"dotenv": "~8.0.0",
- "express": "~4.17.0",
+ "express": "~4.17.1",
"faker": "~4.1.0",
- "graphql": "~14.3.0",
+ "graphql": "~14.3.1",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.2",
- "graphql-shield": "~5.3.5",
+ "graphql-shield": "~5.3.6",
"graphql-tag": "~2.10.1",
"graphql-yoga": "~1.17.4",
"helmet": "~3.18.0",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.11",
- "ms": "~2.1.1",
+ "merge-graphql-schemas": "^1.5.8",
"neo4j-driver": "~1.7.4",
- "neo4j-graphql-js": "~2.6.0",
+ "neo4j-graphql-js": "git+https://github.com/Human-Connection/neo4j-graphql-js.git#temporary_fixes",
"node-fetch": "~2.6.0",
"npm-run-all": "~4.1.5",
"request": "~2.88.0",
@@ -83,27 +82,30 @@
},
"devDependencies": {
"@babel/cli": "~7.4.4",
- "@babel/core": "~7.4.4",
- "@babel/node": "~7.2.2",
+ "@babel/core": "~7.4.5",
+ "@babel/node": "~7.4.5",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
- "@babel/preset-env": "~7.4.4",
+ "@babel/preset-env": "~7.4.5",
"@babel/register": "~7.4.4",
- "apollo-server-testing": "~2.5.0",
+ "apollo-server-testing": "~2.6.2",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.1",
"babel-jest": "~24.8.0",
"chai": "~4.2.0",
"cucumber": "~5.1.0",
"eslint": "~5.16.0",
+ "eslint-config-prettier": "~4.3.0",
"eslint-config-standard": "~12.0.0",
- "eslint-plugin-import": "~2.17.2",
- "eslint-plugin-jest": "~22.5.1",
- "eslint-plugin-node": "~9.0.1",
+ "eslint-plugin-import": "~2.17.3",
+ "eslint-plugin-jest": "~22.6.4",
+ "eslint-plugin-node": "~9.1.0",
+ "eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.1.1",
"eslint-plugin-standard": "~4.0.0",
"graphql-request": "~1.8.2",
"jest": "~24.8.0",
- "nodemon": "~1.19.0",
+ "nodemon": "~1.19.1",
+ "prettier": "~1.17.1",
"supertest": "~4.0.2"
}
-}
\ No newline at end of file
+}
diff --git a/backend/public/uploads/.gitkeep b/backend/public/uploads/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/src/activitypub/ActivityPub.js b/backend/src/activitypub/ActivityPub.js
index 3ce27e109..12671f330 100644
--- a/backend/src/activitypub/ActivityPub.js
+++ b/backend/src/activitypub/ActivityPub.js
@@ -1,20 +1,12 @@
-import {
- extractNameFromId,
- extractDomainFromUrl,
- signAndSend
-} from './utils'
-import {
- isPublicAddressed,
- sendAcceptActivity,
- sendRejectActivity
-} from './utils/activity'
+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 dotenv from 'dotenv'
import Collections from './Collections'
import uuid from 'uuid/v4'
+import CONFIG from '../config'
const debug = require('debug')('ea')
let activityPub = null
@@ -22,182 +14,199 @@ let activityPub = null
export { activityPub }
export default class ActivityPub {
- constructor (activityPubEndpointUri, internalGraphQlUri) {
+ constructor(activityPubEndpointUri, internalGraphQlUri) {
this.endpoint = activityPubEndpointUri
this.dataSource = new NitroDataSource(internalGraphQlUri)
this.collections = new Collections(this.dataSource)
}
- static init (server) {
+ static init(server) {
if (!activityPub) {
- dotenv.config()
- activityPub = new ActivityPub(process.env.CLIENT_URI || 'http://localhost:3000', process.env.GRAPHQL_URI || 'http://localhost:4000')
+ 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')
+ 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')
+ console.log('-> ActivityPub middleware already added to the graphql express server') // eslint-disable-line no-console
}
}
- handleFollowActivity (activity) {
+ 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)
+ 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)
+ let followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
+ activity.object,
+ )
- const followActivity = as.follow()
- .id(activity.id)
- .actor(activity.actor)
- .object(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!')
+ // 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}`)
- 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}`)
+ 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))
- }
- })
+ 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) {
+ 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:
+ case 'Follow':
+ const followActivity = activity.object
+ return this.dataSource.undoFollowActivity(followActivity.actor, followActivity.object)
+ case 'Like':
+ return this.dataSource.deleteShouted(activity)
+ default:
}
}
- handleCreateActivity (activity) {
+ 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:
+ 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) {
+ handleDeleteActivity(activity) {
debug('inside delete')
switch (activity.object.type) {
- case 'Article':
- case 'Note':
- return this.dataSource.deletePost(activity)
- default:
+ case 'Article':
+ case 'Note':
+ return this.dataSource.deletePost(activity)
+ default:
}
}
- handleUpdateActivity (activity) {
+ handleUpdateActivity(activity) {
debug('inside update')
switch (activity.object.type) {
- case 'Note':
- case 'Article':
- return this.dataSource.updatePost(activity)
- default:
+ case 'Note':
+ case 'Article':
+ return this.dataSource.updatePost(activity)
+ default:
}
}
- handleLikeActivity (activity) {
+ handleLikeActivity(activity) {
// TODO differ if activity is an Article/Note/etc.
return this.dataSource.createShouted(activity)
}
- handleDislikeActivity (activity) {
+ handleDislikeActivity(activity) {
// TODO differ if activity is an Article/Note/etc.
return this.dataSource.deleteShouted(activity)
}
- async handleAcceptActivity (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)
+ 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) {
+ 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))
- })
+ request(
+ {
+ url: url,
+ headers: {
+ Accept: 'application/json',
+ },
+ },
+ (err, response, body) => {
+ if (err) {
+ reject(err)
+ }
+ resolve(JSON.parse(body))
+ },
+ )
})
}
- generateStatusId (slug) {
+ generateStatusId(slug) {
return `https://${this.host}/activitypub/users/${slug}/status/${uuid()}`
}
- async sendActivity (activity) {
+ 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) => {
+ sharedInboxEndpoints.map(sharedInbox => {
return this.trySend(activity, fromName, new URL(sharedInbox).host, sharedInbox)
})
- activity.to = activity.to.filter((recipient) => {
- return !(isPublicAddressed({ to: recipient }))
+ activity.to = activity.to.filter(recipient => {
+ return !isPublicAddressed({ to: recipient })
})
// serve the rest
- activity.to.map(async (recipient) => {
+ activity.to.map(async recipient => {
debug('serve rest')
const actorObject = await this.getActorObject(recipient)
return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox)
@@ -207,18 +216,18 @@ export default class ActivityPub {
const actorObject = await this.getActorObject(activity.to)
return this.trySend(activity, fromName, new URL(activity.to).host, actorObject.inbox)
} else if (Array.isArray(activity.to)) {
- activity.to.map(async (recipient) => {
+ activity.to.map(async recipient => {
const actorObject = await this.getActorObject(recipient)
return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox)
})
}
}
- async trySend (activity, fromName, host, url, tries = 5) {
+ async trySend(activity, fromName, host, url, tries = 5) {
try {
return await signAndSend(activity, fromName, host, url)
} catch (e) {
if (tries > 0) {
- setTimeout(function () {
+ setTimeout(function() {
return this.trySend(activity, fromName, host, url, --tries)
}, 20000)
}
diff --git a/backend/src/activitypub/Collections.js b/backend/src/activitypub/Collections.js
index 227e1717b..641db596a 100644
--- a/backend/src/activitypub/Collections.js
+++ b/backend/src/activitypub/Collections.js
@@ -1,28 +1,28 @@
export default class Collections {
- constructor (dataSource) {
+ constructor(dataSource) {
this.dataSource = dataSource
}
- getFollowersCollection (actorId) {
+ getFollowersCollection(actorId) {
return this.dataSource.getFollowersCollection(actorId)
}
- getFollowersCollectionPage (actorId) {
+ getFollowersCollectionPage(actorId) {
return this.dataSource.getFollowersCollectionPage(actorId)
}
- getFollowingCollection (actorId) {
+ getFollowingCollection(actorId) {
return this.dataSource.getFollowingCollection(actorId)
}
- getFollowingCollectionPage (actorId) {
+ getFollowingCollectionPage(actorId) {
return this.dataSource.getFollowingCollectionPage(actorId)
}
- getOutboxCollection (actorId) {
+ getOutboxCollection(actorId) {
return this.dataSource.getOutboxCollection(actorId)
}
- getOutboxCollectionPage (actorId) {
+ getOutboxCollectionPage(actorId) {
return this.dataSource.getOutboxCollectionPage(actorId)
}
}
diff --git a/backend/src/activitypub/NitroDataSource.js b/backend/src/activitypub/NitroDataSource.js
index 0ab6db091..eea37337a 100644
--- a/backend/src/activitypub/NitroDataSource.js
+++ b/backend/src/activitypub/NitroDataSource.js
@@ -2,16 +2,10 @@ import {
throwErrorIfApolloErrorOccurred,
extractIdFromActivityId,
extractNameFromId,
- constructIdFromName
+ constructIdFromName,
} from './utils'
-import {
- createOrderedCollection,
- createOrderedCollectionPage
-} from './utils/collection'
-import {
- createArticleObject,
- isPublicAddressed
-} from './utils/activity'
+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'
@@ -23,35 +17,36 @@ import trunc from 'trunc-html'
const debug = require('debug')('ea:nitro-datasource')
export default class NitroDataSource {
- constructor (uri) {
+ constructor(uri) {
this.uri = uri
const defaultOptions = {
query: {
fetchPolicy: 'network-only',
- errorPolicy: 'all'
- }
+ 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'
+ 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}` : ''
- }
+ Authorization: token ? `Bearer ${token}` : '',
+ },
}
})
this.client = new ApolloClient({
link: authLink.concat(link),
cache: cache,
- defaultOptions
+ defaultOptions,
})
}
- async getFollowersCollection (actorId) {
+ async getFollowersCollection(actorId) {
const slug = extractNameFromId(actorId)
debug(`slug= ${slug}`)
const result = await this.client.query({
@@ -61,7 +56,7 @@ export default class NitroDataSource {
followedByCount
}
}
- `
+ `,
})
debug('successfully fetched followers')
debug(result.data)
@@ -78,7 +73,7 @@ export default class NitroDataSource {
}
}
- async getFollowersCollectionPage (actorId) {
+ async getFollowersCollectionPage(actorId) {
const slug = extractNameFromId(actorId)
debug(`getFollowersPage slug = ${slug}`)
const result = await this.client.query({
@@ -91,7 +86,7 @@ export default class NitroDataSource {
followedByCount
}
}
- `
+ `,
})
debug(result.data)
@@ -104,9 +99,9 @@ export default class NitroDataSource {
followersCollection.totalItems = followersCount
debug(`followers = ${JSON.stringify(followers, null, 2)}`)
await Promise.all(
- followers.map(async (follower) => {
+ followers.map(async follower => {
followersCollection.orderedItems.push(constructIdFromName(follower.slug))
- })
+ }),
)
return followersCollection
@@ -115,7 +110,7 @@ export default class NitroDataSource {
}
}
- async getFollowingCollection (actorId) {
+ async getFollowingCollection(actorId) {
const slug = extractNameFromId(actorId)
const result = await this.client.query({
query: gql`
@@ -124,7 +119,7 @@ export default class NitroDataSource {
followingCount
}
}
- `
+ `,
})
debug(result.data)
@@ -141,7 +136,7 @@ export default class NitroDataSource {
}
}
- async getFollowingCollectionPage (actorId) {
+ async getFollowingCollectionPage(actorId) {
const slug = extractNameFromId(actorId)
const result = await this.client.query({
query: gql`
@@ -153,7 +148,7 @@ export default class NitroDataSource {
followingCount
}
}
- `
+ `,
})
debug(result.data)
@@ -166,9 +161,9 @@ export default class NitroDataSource {
followingCollection.totalItems = followingCount
await Promise.all(
- following.map(async (user) => {
+ following.map(async user => {
followingCollection.orderedItems.push(await constructIdFromName(user.slug))
- })
+ }),
)
return followingCollection
@@ -177,7 +172,7 @@ export default class NitroDataSource {
}
}
- async getOutboxCollection (actorId) {
+ async getOutboxCollection(actorId) {
const slug = extractNameFromId(actorId)
const result = await this.client.query({
query: gql`
@@ -192,7 +187,7 @@ export default class NitroDataSource {
}
}
}
- `
+ `,
})
debug(result.data)
@@ -209,7 +204,7 @@ export default class NitroDataSource {
}
}
- async getOutboxCollectionPage (actorId) {
+ async getOutboxCollectionPage(actorId) {
const slug = extractNameFromId(actorId)
debug(`inside getting outbox collection page => ${slug}`)
const result = await this.client.query({
@@ -232,7 +227,7 @@ export default class NitroDataSource {
}
}
}
- `
+ `,
})
debug(result.data)
@@ -243,9 +238,18 @@ export default class NitroDataSource {
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))
- })
+ 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')
@@ -255,7 +259,7 @@ export default class NitroDataSource {
}
}
- async undoFollowActivity (fromActorId, toActorId) {
+ async undoFollowActivity(fromActorId, toActorId) {
const fromUserId = await this.ensureUser(fromActorId)
const toUserId = await this.ensureUser(toActorId)
const result = await this.client.mutate({
@@ -265,13 +269,13 @@ export default class NitroDataSource {
from { name }
}
}
- `
+ `,
})
debug(`undoFollowActivity result = ${JSON.stringify(result, null, 2)}`)
throwErrorIfApolloErrorOccurred(result)
}
- async saveFollowersCollectionPage (followersCollection, onlyNewestItem = true) {
+ async saveFollowersCollectionPage(followersCollection, onlyNewestItem = true) {
debug('inside saveFollowers')
let orderedItems = followersCollection.orderedItems
const toUserName = extractNameFromId(followersCollection.id)
@@ -279,7 +283,7 @@ export default class NitroDataSource {
orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems
return Promise.all(
- orderedItems.map(async (follower) => {
+ orderedItems.map(async follower => {
debug(`follower = ${follower}`)
const fromUserId = await this.ensureUser(follower)
debug(`fromUserId = ${fromUserId}`)
@@ -291,22 +295,22 @@ export default class NitroDataSource {
from { name }
}
}
- `
+ `,
})
debug(`addUserFollowedBy edge = ${JSON.stringify(result, null, 2)}`)
throwErrorIfApolloErrorOccurred(result)
debug('saveFollowers: added follow edge successfully')
- })
+ }),
)
}
- async saveFollowingCollectionPage (followingCollection, onlyNewestItem = true) {
+ 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) => {
+ orderedItems.map(async following => {
debug(`follower = ${following}`)
const toUserId = await this.ensureUser(following)
debug(`fromUserId = ${fromUserId}`)
@@ -318,33 +322,45 @@ export default class NitroDataSource {
from { name }
}
}
- `
+ `,
})
debug(`addUserFollowing edge = ${JSON.stringify(result, null, 2)}`)
throwErrorIfApolloErrorOccurred(result)
debug('saveFollowing: added follow edge successfully')
- })
+ }),
)
}
- async createPost (activity) {
+ 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)')
+ 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 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}") {
+ CreatePost(content: "${postObject.content}", contentExcerpt: "${trunc(
+ postObject.content,
+ 120,
+ )}", title: "${title}", id: "${postId}", objectId: "${postObject.id}", activityId: "${
+ activity.id
+ }") {
id
}
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
@@ -362,13 +378,13 @@ export default class NitroDataSource {
}
}
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
}
- async deletePost (activity) {
+ async deletePost(activity) {
const result = await this.client.mutate({
mutation: gql`
mutation {
@@ -376,28 +392,30 @@ export default class NitroDataSource {
title
}
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
}
- async updatePost (activity) {
+ 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}") {
+ UpdatePost(content: "${postObject.content}", contentExcerpt: "${
+ trunc(postObject.content, 120).html
+ }", id: "${postId}", updatedAt: "${date}") {
title
}
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
}
- async createShouted (activity) {
+ async createShouted(activity) {
const userId = await this.ensureUser(activity.actor)
const postId = extractIdFromActivityId(activity.object)
const result = await this.client.mutate({
@@ -409,7 +427,7 @@ export default class NitroDataSource {
}
}
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
if (!result.data.AddUserShouted) {
@@ -418,7 +436,7 @@ export default class NitroDataSource {
}
}
- async deleteShouted (activity) {
+ async deleteShouted(activity) {
const userId = await this.ensureUser(activity.actor)
const postId = extractIdFromActivityId(activity.object)
const result = await this.client.mutate({
@@ -430,7 +448,7 @@ export default class NitroDataSource {
}
}
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
if (!result.data.AddUserShouted) {
@@ -439,27 +457,27 @@ export default class NitroDataSource {
}
}
- async getSharedInboxEndpoints () {
+ async getSharedInboxEndpoints() {
const result = await this.client.query({
query: gql`
query {
- SharedInboxEndpoint {
- uri
- }
+ SharedInboxEndpoint {
+ uri
+ }
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
return result.data.SharedInboxEnpoint
}
- async addSharedInboxEndpoint (uri) {
+ async addSharedInboxEndpoint(uri) {
try {
const result = await this.client.mutate({
mutation: gql`
mutation {
CreateSharedInboxEndpoint(uri: "${uri}")
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
return true
@@ -468,16 +486,18 @@ export default class NitroDataSource {
}
}
- async createComment (activity) {
+ async createComment(activity) {
const postObject = activity.object
let result = await this.client.mutate({
mutation: gql`
mutation {
- CreateComment(content: "${postObject.content}", activityId: "${extractIdFromActivityId(activity.id)}") {
+ CreateComment(content: "${
+ postObject.content
+ }", activityId: "${extractIdFromActivityId(activity.id)}") {
id
}
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
@@ -485,11 +505,13 @@ export default class NitroDataSource {
const result2 = await this.client.mutate({
mutation: gql`
mutation {
- AddCommentAuthor(from: {id: "${result.data.CreateComment.id}"}, to: {id: "${toUserId}"}) {
+ AddCommentAuthor(from: {id: "${
+ result.data.CreateComment.id
+ }"}, to: {id: "${toUserId}"}) {
id
}
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result2)
@@ -497,11 +519,13 @@ export default class NitroDataSource {
result = await this.client.mutate({
mutation: gql`
mutation {
- AddCommentPost(from: { id: "${result.data.CreateComment.id}", to: { id: "${postId}" }}) {
+ AddCommentPost(from: { id: "${
+ result.data.CreateComment.id
+ }", to: { id: "${postId}" }}) {
id
}
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
@@ -513,7 +537,7 @@ export default class NitroDataSource {
* @param actorId
* @returns {Promise<*>}
*/
- async ensureUser (actorId) {
+ async ensureUser(actorId) {
debug(`inside ensureUser = ${actorId}`)
const name = extractNameFromId(actorId)
const queryResult = await this.client.query({
@@ -523,10 +547,14 @@ export default class NitroDataSource {
id
}
}
- `
+ `,
})
- if (queryResult.data && Array.isArray(queryResult.data.User) && queryResult.data.User.length > 0) {
+ 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
@@ -534,7 +562,10 @@ export default class NitroDataSource {
debug('ensureUser: user not exists.. createUser')
// user does not exist.. create it
const pw = crypto.randomBytes(16).toString('hex')
- const slug = name.toLowerCase().split(' ').join('-')
+ const slug = name
+ .toLowerCase()
+ .split(' ')
+ .join('-')
const result = await this.client.mutate({
mutation: gql`
mutation {
@@ -542,7 +573,7 @@ export default class NitroDataSource {
id
}
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
diff --git a/backend/src/activitypub/routes/inbox.js b/backend/src/activitypub/routes/inbox.js
index f9cfb3794..b31b89ed4 100644
--- a/backend/src/activitypub/routes/inbox.js
+++ b/backend/src/activitypub/routes/inbox.js
@@ -7,24 +7,24 @@ const router = express.Router()
// Shared Inbox endpoint (federated Server)
// For now its only able to handle Note Activities!!
-router.post('/', async function (req, res, next) {
+router.post('/', async function(req, res, next) {
debug(`Content-Type = ${req.get('Content-Type')}`)
debug(`body = ${JSON.stringify(req.body, null, 2)}`)
debug(`Request headers = ${JSON.stringify(req.headers, null, 2)}`)
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 '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
diff --git a/backend/src/activitypub/routes/index.js b/backend/src/activitypub/routes/index.js
index 24898e766..c7d31f1c4 100644
--- a/backend/src/activitypub/routes/index.js
+++ b/backend/src/activitypub/routes/index.js
@@ -7,23 +7,21 @@ import verify from './verify'
const router = express.Router()
-router.use('/.well-known/webFinger',
- cors(),
- express.urlencoded({ extended: true }),
- webFinger
-)
-router.use('/activitypub/users',
+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
+ user,
)
-router.use('/activitypub/inbox',
+router.use(
+ '/activitypub/inbox',
cors(),
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
express.urlencoded({ extended: true }),
verify,
- inbox
+ inbox,
)
export default router
diff --git a/backend/src/activitypub/routes/serveUser.js b/backend/src/activitypub/routes/serveUser.js
index f65876741..6f4472235 100644
--- a/backend/src/activitypub/routes/serveUser.js
+++ b/backend/src/activitypub/routes/serveUser.js
@@ -2,7 +2,7 @@ import { createActor } from '../utils/actor'
const gql = require('graphql-tag')
const debug = require('debug')('ea:serveUser')
-export async function serveUser (req, res, next) {
+export async function serveUser(req, res, next) {
let name = req.params.name
if (name.startsWith('@')) {
@@ -10,21 +10,32 @@ export async function serveUser (req, res, next) {
}
debug(`name = ${name}`)
- const result = await req.app.get('ap').dataSource.client.query({
- query: gql`
+ 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}`) })
+ `,
+ })
+ .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'])}`)
+ 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')) {
diff --git a/backend/src/activitypub/routes/user.js b/backend/src/activitypub/routes/user.js
index 017891e61..9dc9b5071 100644
--- a/backend/src/activitypub/routes/user.js
+++ b/backend/src/activitypub/routes/user.js
@@ -7,7 +7,7 @@ import verify from './verify'
const router = express.Router()
const debug = require('debug')('ea:user')
-router.get('/:name', async function (req, res, next) {
+router.get('/:name', async function(req, res, next) {
debug('inside user.js -> serveUser')
await serveUser(req, res, next)
})
@@ -45,24 +45,24 @@ router.get('/:name/outbox', (req, res) => {
}
})
-router.post('/:name/inbox', verify, async function (req, res, next) {
+router.post('/:name/inbox', verify, async function(req, res, next) {
debug(`body = ${JSON.stringify(req.body, null, 2)}`)
debug(`actorId = ${req.body.actor}`)
// const result = await saveActorId(req.body.actor)
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 '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
diff --git a/backend/src/activitypub/routes/verify.js b/backend/src/activitypub/routes/verify.js
index bb5850b3e..33603805f 100644
--- a/backend/src/activitypub/routes/verify.js
+++ b/backend/src/activitypub/routes/verify.js
@@ -4,7 +4,12 @@ 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)) {
+ if (
+ await verifySignature(
+ `${req.protocol}://${req.hostname}:${req.app.get('port')}${req.originalUrl}`,
+ req.headers,
+ )
+ ) {
debug('verify = true')
next()
} else {
diff --git a/backend/src/activitypub/routes/webFinger.js b/backend/src/activitypub/routes/webFinger.js
index 8def32328..7d52c69cd 100644
--- a/backend/src/activitypub/routes/webFinger.js
+++ b/backend/src/activitypub/routes/webFinger.js
@@ -4,10 +4,14 @@ import gql from 'graphql-tag'
const router = express.Router()
-router.get('/', async function (req, res) {
+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.')
+ 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]
@@ -21,7 +25,7 @@ router.get('/', async function (req, res) {
slug
}
}
- `
+ `,
})
} catch (error) {
return res.status(500).json({ error })
diff --git a/backend/src/activitypub/security/httpSignature.spec.js b/backend/src/activitypub/security/httpSignature.spec.js
index d40c38242..0c6fbb8b5 100644
--- a/backend/src/activitypub/security/httpSignature.spec.js
+++ b/backend/src/activitypub/security/httpSignature.spec.js
@@ -14,9 +14,9 @@ describe('activityPub/security', () => {
privateKey = pair.privateKey
publicKey = pair.publicKey
headers = {
- 'Date': '2019-03-08T14:35:45.759Z',
- 'Host': 'democracy-app.de',
- 'Content-Type': 'application/json'
+ Date: '2019-03-08T14:35:45.759Z',
+ Host: 'democracy-app.de',
+ 'Content-Type': 'application/json',
}
})
@@ -27,13 +27,23 @@ describe('activityPub/security', () => {
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')
+ 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 })
+ 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"')
+ expect(httpSignature).toContain(
+ 'keyId="https://human-connection.org/activitypub/users/lea#main-key"',
+ )
})
it('contains default algorithm "rsa-sha256"', () => {
@@ -54,13 +64,19 @@ describe('activityPub/security', () => {
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 })
+ 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
- }
+ 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)))
@@ -68,7 +84,9 @@ describe('activityPub/security', () => {
})
it('resolves false', async () => {
- await expect(verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers)).resolves.toEqual(false)
+ await expect(
+ verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers),
+ ).resolves.toEqual(false)
})
describe('valid signature', () => {
@@ -77,7 +95,9 @@ describe('activityPub/security', () => {
})
it('resolves true', async () => {
- await expect(verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers)).resolves.toEqual(true)
+ await expect(
+ verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers),
+ ).resolves.toEqual(true)
})
})
})
diff --git a/backend/src/activitypub/security/index.js b/backend/src/activitypub/security/index.js
index fdb1e27c6..9b48b7ed9 100644
--- a/backend/src/activitypub/security/index.js
+++ b/backend/src/activitypub/security/index.js
@@ -1,47 +1,55 @@
-import dotenv from 'dotenv'
-import { resolve } from 'path'
+// 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')
-dotenv.config({ path: resolve('src', 'activitypub', '.env') })
+// TODO Does this reference a local config? Why?
+// dotenv.config({ path: resolve('src', 'activitypub', '.env') })
-export function generateRsaKeyPair (options = {}) {
- const { passphrase = process.env.PRIVATE_KEY_PASSPHRASE } = options
+export function generateRsaKeyPair(options = {}) {
+ const { passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE } = options
return crypto.generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
- format: 'pem'
+ format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
- passphrase
- }
+ passphrase,
+ },
})
}
// signing
-export function createSignature (options) {
+export function createSignature(options) {
const {
- privateKey, keyId, url,
+ privateKey,
+ keyId,
+ url,
headers = {},
algorithm = 'rsa-sha256',
- passphrase = process.env.PRIVATE_KEY_PASSPHRASE
+ passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE,
} = options
- if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) }
+ 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() }, '')
+ 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) {
+export function verifySignature(url, headers) {
return new Promise((resolve, reject) => {
const signatureHeader = headers['signature'] ? headers['signature'] : headers['Signature']
if (!signatureHeader) {
@@ -61,40 +69,47 @@ export function verifySignature (url, headers) {
const usedHeaders = headersString.split(' ')
const verifyHeaders = {}
- Object.keys(headers).forEach((key) => {
+ Object.keys(headers).forEach(key => {
if (usedHeaders.includes(key.toLowerCase())) {
verifyHeaders[key.toLowerCase()] = headers[key]
}
})
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))
- })
+ 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) {
+function constructSigningString(url, headers) {
const urlObj = new URL(url)
- let signingString = `(request-target): post ${urlObj.pathname}${urlObj.search !== '' ? urlObj.search : ''}`
+ 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}`) }
+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')
@@ -103,14 +118,16 @@ function httpVerify (pubKey, signature, signingString, algorithm) {
// 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) => {
+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 }
+ if (key === 'headers') {
+ firstEqualIndex += 17
+ }
return keyString.substring(firstEqualIndex + 2, keyString.length - 1)
}
@@ -151,4 +168,5 @@ export const SUPPORTED_HASH_ALGORITHMS = [
'sha512WithRSAEncryption',
'ssl3-md5',
'ssl3-sha1',
- 'whirlpool']
+ 'whirlpool',
+]
diff --git a/backend/src/activitypub/utils/activity.js b/backend/src/activitypub/utils/activity.js
index 57b6dfb83..baf13e1bf 100644
--- a/backend/src/activitypub/utils/activity.js
+++ b/backend/src/activitypub/utils/activity.js
@@ -6,45 +6,45 @@ import as from 'activitystrea.ms'
import gql from 'graphql-tag'
const debug = require('debug')('ea:utils:activity')
-export function createNoteObject (text, name, id, published) {
+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'
- }
+ 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) {
+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'
- }
+ 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) {
+export async function getActorId(name) {
const result = await activityPub.dataSource.client.query({
query: gql`
query {
@@ -52,7 +52,7 @@ export async function getActorId (name) {
actorId
}
}
- `
+ `,
})
throwErrorIfApolloErrorOccurred(result)
if (Array.isArray(result.data.User) && result.data.User[0]) {
@@ -62,9 +62,12 @@ export async function getActorId (name) {
}
}
-export function sendAcceptActivity (theBody, name, targetDomain, url) {
+export function sendAcceptActivity(theBody, name, targetDomain, url) {
as.accept()
- .id(`${activityPub.endpoint}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
+ .id(
+ `${activityPub.endpoint}/activitypub/users/${name}/status/` +
+ crypto.randomBytes(16).toString('hex'),
+ )
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
.object(theBody)
.prettyWrite((err, doc) => {
@@ -77,9 +80,12 @@ export function sendAcceptActivity (theBody, name, targetDomain, url) {
})
}
-export function sendRejectActivity (theBody, name, targetDomain, url) {
+export function sendRejectActivity(theBody, name, targetDomain, url) {
as.reject()
- .id(`${activityPub.endpoint}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex'))
+ .id(
+ `${activityPub.endpoint}/activitypub/users/${name}/status/` +
+ crypto.randomBytes(16).toString('hex'),
+ )
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
.object(theBody)
.prettyWrite((err, doc) => {
@@ -92,7 +98,7 @@ export function sendRejectActivity (theBody, name, targetDomain, url) {
})
}
-export function isPublicAddressed (postObject) {
+export function isPublicAddressed(postObject) {
if (typeof postObject.to === 'string') {
postObject.to = [postObject.to]
}
@@ -102,7 +108,9 @@ export function isPublicAddressed (postObject) {
if (Array.isArray(postObject)) {
postObject.to = postObject
}
- return postObject.to.includes('Public') ||
+ return (
+ postObject.to.includes('Public') ||
postObject.to.includes('as:Public') ||
postObject.to.includes('https://www.w3.org/ns/activitystreams#Public')
+ )
}
diff --git a/backend/src/activitypub/utils/actor.js b/backend/src/activitypub/utils/actor.js
index 27612517b..a08065778 100644
--- a/backend/src/activitypub/utils/actor.js
+++ b/backend/src/activitypub/utils/actor.js
@@ -1,41 +1,38 @@
import { activityPub } from '../ActivityPub'
-export function createActor (name, pubkey) {
+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`
+ '@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,
},
- 'publicKey': {
- 'id': `${activityPub.endpoint}/activitypub/users/${name}#main-key`,
- 'owner': `${activityPub.endpoint}/activitypub/users/${name}`,
- 'publicKeyPem': pubkey
- }
}
}
-export function createWebFinger (name) {
+export function createWebFinger(name) {
const { host } = new URL(activityPub.endpoint)
return {
- 'subject': `acct:${name}@${host}`,
- 'links': [
+ subject: `acct:${name}@${host}`,
+ links: [
{
- 'rel': 'self',
- 'type': 'application/activity+json',
- 'href': `${activityPub.endpoint}/activitypub/users/${name}`
- }
- ]
+ rel: 'self',
+ type: 'application/activity+json',
+ href: `${activityPub.endpoint}/activitypub/users/${name}`,
+ },
+ ],
}
}
diff --git a/backend/src/activitypub/utils/collection.js b/backend/src/activitypub/utils/collection.js
index e3a63c74d..29cf69ac2 100644
--- a/backend/src/activitypub/utils/collection.js
+++ b/backend/src/activitypub/utils/collection.js
@@ -2,68 +2,71 @@ import { activityPub } from '../ActivityPub'
import { constructIdFromName } from './index'
const debug = require('debug')('ea:utils:collections')
-export function createOrderedCollection (name, collectionName) {
+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
+ 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) {
+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': []
+ 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) {
+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 'followers':
+ attachThenCatch(activityPub.collections.getFollowersCollection(id), res)
+ break
- case 'followersPage':
- attachThenCatch(activityPub.collections.getFollowersCollectionPage(id), res)
- break
+ case 'followersPage':
+ attachThenCatch(activityPub.collections.getFollowersCollectionPage(id), res)
+ break
- case 'following':
- attachThenCatch(activityPub.collections.getFollowingCollection(id), res)
- break
+ case 'following':
+ attachThenCatch(activityPub.collections.getFollowingCollection(id), res)
+ break
- case 'followingPage':
- attachThenCatch(activityPub.collections.getFollowingCollectionPage(id), res)
- break
+ case 'followingPage':
+ attachThenCatch(activityPub.collections.getFollowingCollectionPage(id), res)
+ break
- case 'outbox':
- attachThenCatch(activityPub.collections.getOutboxCollection(id), res)
- break
+ case 'outbox':
+ attachThenCatch(activityPub.collections.getOutboxCollection(id), res)
+ break
- case 'outboxPage':
- attachThenCatch(activityPub.collections.getOutboxCollectionPage(id), res)
- break
+ case 'outboxPage':
+ attachThenCatch(activityPub.collections.getOutboxCollectionPage(id), res)
+ break
- default:
- res.status(500).end()
+ default:
+ res.status(500).end()
}
}
-function attachThenCatch (promise, res) {
+function attachThenCatch(promise, res) {
return promise
- .then((collection) => {
- res.status(200).contentType('application/activity+json').send(collection)
+ .then(collection => {
+ res
+ .status(200)
+ .contentType('application/activity+json')
+ .send(collection)
})
- .catch((err) => {
+ .catch(err => {
debug(`error getting a Collection: = ${err}`)
res.status(500).end()
})
diff --git a/backend/src/activitypub/utils/index.js b/backend/src/activitypub/utils/index.js
index a83dcc829..3927f4056 100644
--- a/backend/src/activitypub/utils/index.js
+++ b/backend/src/activitypub/utils/index.js
@@ -2,9 +2,10 @@ 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) {
+export function extractNameFromId(uri) {
const urlObject = new URL(uri)
const pathname = urlObject.pathname
const splitted = pathname.split('/')
@@ -12,31 +13,33 @@ export function extractNameFromId (uri) {
return splitted[splitted.indexOf('users') + 1]
}
-export function extractIdFromActivityId (uri) {
+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) {
+export function constructIdFromName(name, fromDomain = activityPub.endpoint) {
return `${fromDomain}/activitypub/users/${name}`
}
-export function extractDomainFromUrl (url) {
+export function extractDomainFromUrl(url) {
return new URL(url).host
}
-export function throwErrorIfApolloErrorOccurred (result) {
+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}`)
+ throw new Error(
+ `${result.error.message ? result.error.message : result.error.errors[0].message}`,
+ )
}
}
-export function signAndSend (activity, fromName, targetDomain, url) {
+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 = ${process.env.PRIVATE_KEY_PASSPHRASE}`)
+ debug(`passhprase = ${CONFIG.PRIVATE_KEY_PASSPHRASE}`)
return new Promise(async (resolve, reject) => {
debug('inside signAndSend')
// get the private key
@@ -47,7 +50,7 @@ export function signAndSend (activity, fromName, targetDomain, url) {
privateKey
}
}
- `
+ `,
})
if (result.error) {
@@ -69,34 +72,38 @@ export function signAndSend (activity, fromName, targetDomain, url) {
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'
+ 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),
},
- 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()
- }
- })
+ (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/backend/src/bootstrap/directives.js b/backend/src/bootstrap/directives.js
index 8c392ed46..93a7574fb 100644
--- a/backend/src/bootstrap/directives.js
+++ b/backend/src/bootstrap/directives.js
@@ -1,15 +1,11 @@
import {
GraphQLLowerCaseDirective,
GraphQLTrimDirective,
- GraphQLDefaultToDirective
+ GraphQLDefaultToDirective,
} from 'graphql-custom-directives'
-export default function applyDirectives (augmentedSchema) {
- const directives = [
- GraphQLLowerCaseDirective,
- GraphQLTrimDirective,
- GraphQLDefaultToDirective
- ]
+export default function applyDirectives(augmentedSchema) {
+ const directives = [GraphQLLowerCaseDirective, GraphQLTrimDirective, GraphQLDefaultToDirective]
augmentedSchema._directives.push.apply(augmentedSchema._directives, directives)
return augmentedSchema
diff --git a/backend/src/bootstrap/neo4j.js b/backend/src/bootstrap/neo4j.js
index 935449a0a..bfa68acf3 100644
--- a/backend/src/bootstrap/neo4j.js
+++ b/backend/src/bootstrap/neo4j.js
@@ -1,15 +1,13 @@
import { v1 as neo4j } from 'neo4j-driver'
-import dotenv from 'dotenv'
-
-dotenv.config()
+import CONFIG from './../config'
let driver
-export function getDriver (options = {}) {
+export function getDriver(options = {}) {
const {
- uri = process.env.NEO4J_URI || 'bolt://localhost:7687',
- username = process.env.NEO4J_USERNAME || 'neo4j',
- password = process.env.NEO4J_PASSWORD || 'neo4j'
+ uri = CONFIG.NEO4J_URI,
+ username = CONFIG.NEO4J_USERNAME,
+ password = CONFIG.NEO4J_PASSWORD,
} = options
if (!driver) {
driver = neo4j.driver(uri, neo4j.auth.basic(username, password))
diff --git a/backend/src/bootstrap/scalars.js b/backend/src/bootstrap/scalars.js
index 813bd5051..eb6d3739b 100644
--- a/backend/src/bootstrap/scalars.js
+++ b/backend/src/bootstrap/scalars.js
@@ -1,10 +1,6 @@
-import {
- GraphQLDate,
- GraphQLTime,
- GraphQLDateTime
-} from 'graphql-iso-date'
+import { GraphQLDate, GraphQLTime, GraphQLDateTime } from 'graphql-iso-date'
-export default function applyScalars (augmentedSchema) {
+export default function applyScalars(augmentedSchema) {
augmentedSchema._typeMap.Date = GraphQLDate
augmentedSchema._typeMap.Time = GraphQLTime
augmentedSchema._typeMap.DateTime = GraphQLDateTime
diff --git a/backend/src/config/index.js b/backend/src/config/index.js
new file mode 100644
index 000000000..aed6f7c1c
--- /dev/null
+++ b/backend/src/config/index.js
@@ -0,0 +1,35 @@
+import dotenv from 'dotenv'
+
+dotenv.config()
+
+export const requiredConfigs = {
+ MAPBOX_TOKEN: process.env.MAPBOX_TOKEN,
+ JWT_SECRET: process.env.JWT_SECRET,
+ PRIVATE_KEY_PASSPHRASE: process.env.PRIVATE_KEY_PASSPHRASE,
+}
+
+export const neo4jConfigs = {
+ NEO4J_URI: process.env.NEO4J_URI || 'bolt://localhost:7687',
+ NEO4J_USERNAME: process.env.NEO4J_USERNAME || 'neo4j',
+ NEO4J_PASSWORD: process.env.NEO4J_PASSWORD || 'neo4j',
+}
+
+export const serverConfigs = {
+ GRAPHQL_PORT: process.env.GRAPHQL_PORT || 4000,
+ CLIENT_URI: process.env.CLIENT_URI || 'http://localhost:3000',
+ GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000',
+}
+
+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,
+ ...neo4jConfigs,
+ ...serverConfigs,
+ ...developmentConfigs,
+}
diff --git a/backend/src/graphql-schema.js b/backend/src/graphql-schema.js
deleted file mode 100644
index bad277721..000000000
--- a/backend/src/graphql-schema.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import fs from 'fs'
-import path from 'path'
-
-import userManagement from './resolvers/user_management.js'
-import statistics from './resolvers/statistics.js'
-import reports from './resolvers/reports.js'
-import posts from './resolvers/posts.js'
-import moderation from './resolvers/moderation.js'
-import follow from './resolvers/follow.js'
-import shout from './resolvers/shout.js'
-import rewards from './resolvers/rewards.js'
-import socialMedia from './resolvers/socialMedia.js'
-import notifications from './resolvers/notifications'
-import comments from './resolvers/comments'
-
-export const typeDefs = fs
- .readFileSync(
- process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql')
- )
- .toString('utf-8')
-
-export const resolvers = {
- Query: {
- ...statistics.Query,
- ...userManagement.Query,
- ...notifications.Query,
- ...comments.Query
- },
- Mutation: {
- ...userManagement.Mutation,
- ...reports.Mutation,
- ...posts.Mutation,
- ...moderation.Mutation,
- ...follow.Mutation,
- ...shout.Mutation,
- ...rewards.Mutation,
- ...socialMedia.Mutation,
- ...notifications.Mutation,
- ...comments.Mutation
- }
-}
diff --git a/backend/src/helpers/asyncForEach.js b/backend/src/helpers/asyncForEach.js
index 1f05ea915..5577cce14 100644
--- a/backend/src/helpers/asyncForEach.js
+++ b/backend/src/helpers/asyncForEach.js
@@ -5,7 +5,7 @@
* @param callback
* @returns {Promise Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock. Something inspirational about @bob-der-baumeister and @jenny-rostock.
'
- )
+ .replace(/<\/(p|div|th|tr)>\s*(
\s*)+\s*<(p|div|th|tr)>/gim, '
')
// remove additional linebreaks inside p tags
- .replace(
- /<[a-z-]+>(<[a-z-]+>)*\s*(
\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim,
- ''
- )
+ .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 @@ -138,5 +150,5 @@ export default { Query: async (resolve, root, args, context, info) => { const result = await resolve(root, args, context, info) return walkRecursive(result, fields, clean) - } + }, } diff --git a/backend/src/mocks/index.js b/backend/src/mocks/index.js index a87ccfbbc..7b453c8c6 100644 --- a/backend/src/mocks/index.js +++ b/backend/src/mocks/index.js @@ -1,15 +1,14 @@ - import faker from 'faker' export default { User: () => ({ name: () => `${faker.name.firstName()} ${faker.name.lastName()}`, - email: () => `${faker.internet.email()}` + 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) - }) + contentExcerpt: () => faker.lorem.paragraphs(1), + }), } diff --git a/backend/src/resolvers/comments.spec.js b/backend/src/resolvers/comments.spec.js deleted file mode 100644 index 7784a889f..000000000 --- a/backend/src/resolvers/comments.spec.js +++ /dev/null @@ -1,211 +0,0 @@ -import Factory from '../seed/factories' -import { GraphQLClient } from 'graphql-request' -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('Comment Functionality', () => { - const createCommentMutation = ` - mutation($postId: ID, $content: String!) { - CreateComment(postId: $postId, content: $content) { - id - content - } - } - ` - const createPostMutation = ` - mutation($id: ID!, $title: String!, $content: String!) { - CreatePost(id: $id, title: $title, content: $content) { - id - } - } - ` - const commentQueryForPostId = ` - query($content: String) { - Comment(content: $content) { - postId - } - } - ` - 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('updates a comment', async () => { - await client.request(createCommentMutation, createCommentVariables) - - const updateCommentMutation = ` - mutation($postId: ID, $content: String!, $id: ID!) { - UpdateComment(postId: $postId, content: $content, id: $id) { - id - content - } - } - ` - let updateCommentVariables = { - postId: 'p1', - content: 'Comment is updated', - id: 'c8' - } - - const expected = { - UpdateComment: { - content: 'Comment is updated' - } - } - - await expect( - client.request(updateCommentMutation, updateCommentVariables) - ).resolves.toMatchObject(expected) - }) - - it('assigns the authenticated user as author', async () => { - await client.request(createCommentMutation, createCommentVariables) - - const { User } = await client.request(`{ - 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!') - }) - - it('does not create the comment with the postId as an attribute', async () => { - const commentQueryVariablesByContent = { - content: 'I\'m authorised to comment' - } - - await client.request(createCommentMutation, createCommentVariables) - const { Comment } = await client.request( - commentQueryForPostId, - commentQueryVariablesByContent - ) - expect(Comment).toEqual([{ postId: null }]) - }) - }) -}) diff --git a/backend/src/resolvers/posts.js b/backend/src/resolvers/posts.js deleted file mode 100644 index 5b06c38fa..000000000 --- a/backend/src/resolvers/posts.js +++ /dev/null @@ -1,22 +0,0 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' - -export default { - Mutation: { - CreatePost: async (object, params, context, resolveInfo) => { - const result = await neo4jgraphql(object, params, context, resolveInfo, false) - - const session = context.driver.session() - await session.run( - 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' + - 'MERGE (post)<-[:WROTE]-(author) ' + - 'RETURN author', { - userId: context.user.id, - postId: result.id - } - ) - session.close() - - return result - } - } -} diff --git a/backend/src/resolvers/socialMedia.spec.js b/backend/src/resolvers/socialMedia.spec.js deleted file mode 100644 index 9d1d76726..000000000 --- a/backend/src/resolvers/socialMedia.spec.js +++ /dev/null @@ -1,101 +0,0 @@ -import Factory from '../seed/factories' -import { GraphQLClient } from 'graphql-request' -import { host, login } from '../jest/helpers' - -const factory = Factory() - -describe('CreateSocialMedia', () => { - let client - let headers - const mutationC = ` - mutation($url: String!) { - CreateSocialMedia(url: $url) { - id - url - } - } - ` - const mutationD = ` - 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/backend/src/resolvers/statistics.js b/backend/src/resolvers/statistics.js deleted file mode 100644 index 17c4be956..000000000 --- a/backend/src/resolvers/statistics.js +++ /dev/null @@ -1,67 +0,0 @@ -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/backend/src/schema/index.js b/backend/src/schema/index.js new file mode 100644 index 000000000..d294d8aba --- /dev/null +++ b/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/backend/src/resolvers/badges.spec.js b/backend/src/schema/resolvers/badges.spec.js similarity index 80% rename from backend/src/resolvers/badges.spec.js rename to backend/src/schema/resolvers/badges.spec.js index 1966ce241..a0dbafe00 100644 --- a/backend/src/resolvers/badges.spec.js +++ b/backend/src/schema/resolvers/badges.spec.js @@ -1,6 +1,6 @@ -import Factory from '../seed/factories' import { GraphQLClient } from 'graphql-request' -import { host, login } from '../jest/helpers' +import Factory from '../../seed/factories' +import { host, login } from '../../jest/helpers' const factory = Factory() let client @@ -10,17 +10,17 @@ describe('badges', () => { await factory.create('User', { email: 'user@example.org', role: 'user', - password: '1234' + password: '1234', }) await factory.create('User', { id: 'u2', role: 'moderator', - email: 'moderator@example.org' + email: 'moderator@example.org', }) await factory.create('User', { id: 'u3', role: 'admin', - email: 'admin@example.org' + email: 'admin@example.org', }) }) @@ -34,15 +34,15 @@ describe('badges', () => { key: 'indiegogo_en_racoon', type: 'crowdfunding', status: 'permanent', - icon: '/img/badges/indiegogo_en_racoon.svg' + icon: '/img/badges/indiegogo_en_racoon.svg', } const mutation = ` mutation( $id: ID $key: String! - $type: BadgeTypeEnum! - $status: BadgeStatusEnum! + $type: BadgeType! + $status: BadgeStatus! $icon: String! ) { CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) { @@ -58,9 +58,7 @@ describe('badges', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { client = new GraphQLClient(host) - await expect( - client.request(mutation, variables) - ).rejects.toThrow('Not Authorised') + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') }) }) @@ -76,8 +74,8 @@ describe('badges', () => { id: 'b1', key: 'indiegogo_en_racoon', status: 'permanent', - type: 'crowdfunding' - } + type: 'crowdfunding', + }, } await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) @@ -90,9 +88,7 @@ describe('badges', () => { }) it('throws authorization error', async () => { - await expect( - client.request(mutation, variables) - ).rejects.toThrow('Not Authorised') + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') }) }) }) @@ -104,7 +100,7 @@ describe('badges', () => { }) const variables = { id: 'b1', - key: 'whatever' + key: 'whatever', } const mutation = ` @@ -119,9 +115,7 @@ describe('badges', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { client = new GraphQLClient(host) - await expect( - client.request(mutation, variables) - ).rejects.toThrow('Not Authorised') + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') }) }) @@ -132,9 +126,7 @@ describe('badges', () => { }) it('throws authorization error', async () => { - await expect( - client.request(mutation, variables) - ).rejects.toThrow('Not Authorised') + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') }) }) @@ -147,8 +139,8 @@ describe('badges', () => { const expected = { UpdateBadge: { id: 'b1', - key: 'whatever' - } + key: 'whatever', + }, } await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) @@ -161,7 +153,7 @@ describe('badges', () => { await factory.create('Badge', { id: 'b1' }) }) const variables = { - id: 'b1' + id: 'b1', } const mutation = ` @@ -175,9 +167,7 @@ describe('badges', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { client = new GraphQLClient(host) - await expect( - client.request(mutation, variables) - ).rejects.toThrow('Not Authorised') + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') }) }) @@ -188,9 +178,7 @@ describe('badges', () => { }) it('throws authorization error', async () => { - await expect( - client.request(mutation, variables) - ).rejects.toThrow('Not Authorised') + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') }) }) @@ -202,8 +190,8 @@ describe('badges', () => { it('deletes a badge', async () => { const expected = { DeleteBadge: { - id: 'b1' - } + id: 'b1', + }, } await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) diff --git a/backend/src/resolvers/comments.js b/backend/src/schema/resolvers/comments.js similarity index 80% rename from backend/src/resolvers/comments.js rename to backend/src/schema/resolvers/comments.js index 2515ba36a..f28ef2792 100644 --- a/backend/src/resolvers/comments.js +++ b/backend/src/schema/resolvers/comments.js @@ -19,8 +19,8 @@ export default { MATCH (post:Post {id: $postId}) RETURN post`, { - postId - } + postId, + }, ) const [post] = postQueryRes.records.map(record => { return record.get('post') @@ -29,14 +29,7 @@ export default { if (!post) { throw new UserInputError(NO_POST_ERR_MESSAGE) } - - const comment = await neo4jgraphql( - object, - params, - context, - resolveInfo, - false - ) + const comment = await neo4jgraphql(object, params, context, resolveInfo, false) await session.run( ` @@ -46,8 +39,8 @@ export default { { userId: context.user.id, postId, - commentId: comment.id - } + commentId: comment.id, + }, ) session.close() @@ -55,6 +48,11 @@ export default { }, UpdateComment: async (object, params, context, resolveInfo) => { await neo4jgraphql(object, params, context, resolveInfo, false) - } - } + }, + DeleteComment: async (object, params, context, resolveInfo) => { + const comment = await neo4jgraphql(object, params, context, resolveInfo, false) + + return comment + }, + }, } diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js new file mode 100644 index 000000000..55b946bb9 --- /dev/null +++ b/backend/src/schema/resolvers/comments.spec.js @@ -0,0 +1,299 @@ +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 + } + } + ` + const commentQueryForPostId = gql` + query($content: String) { + Comment(content: $content) { + postId + } + } + ` + 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!') + }) + + it('does not create the comment with the postId as an attribute', async () => { + const commentQueryVariablesByContent = { + content: "I'm authorised to comment", + } + + await client.request(createCommentMutation, createCommentVariables) + const { Comment } = await client.request( + commentQueryForPostId, + commentQueryVariablesByContent, + ) + expect(Comment).toEqual([ + { + postId: null, + }, + ]) + }) + }) +}) + +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/backend/src/schema/resolvers/fileUpload/index.js b/backend/src/schema/resolvers/fileUpload/index.js new file mode 100644 index 000000000..c37d87e39 --- /dev/null +++ b/backend/src/schema/resolvers/fileUpload/index.js @@ -0,0 +1,27 @@ +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/backend/src/schema/resolvers/fileUpload/spec.js b/backend/src/schema/resolvers/fileUpload/spec.js new file mode 100644 index 000000000..5767d6457 --- /dev/null +++ b/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/backend/src/resolvers/follow.js b/backend/src/schema/resolvers/follow.js similarity index 92% rename from backend/src/resolvers/follow.js rename to backend/src/schema/resolvers/follow.js index df7b58891..4e9a3b27d 100644 --- a/backend/src/resolvers/follow.js +++ b/backend/src/schema/resolvers/follow.js @@ -12,8 +12,8 @@ export default { { id, type, - userId: context.user.id - } + userId: context.user.id, + }, ) const [isFollowed] = transactionRes.records.map(record => { @@ -37,8 +37,8 @@ export default { { id, type, - userId: context.user.id - } + userId: context.user.id, + }, ) const [isFollowed] = transactionRes.records.map(record => { return record.get('isFollowed') @@ -46,6 +46,6 @@ export default { session.close() return isFollowed - } - } + }, + }, } diff --git a/backend/src/resolvers/follow.spec.js b/backend/src/schema/resolvers/follow.spec.js similarity index 70% rename from backend/src/resolvers/follow.spec.js rename to backend/src/schema/resolvers/follow.spec.js index 081e49081..d29e17938 100644 --- a/backend/src/resolvers/follow.spec.js +++ b/backend/src/schema/resolvers/follow.spec.js @@ -1,17 +1,17 @@ -import Factory from '../seed/factories' import { GraphQLClient } from 'graphql-request' -import { host, login } from '../jest/helpers' +import Factory from '../../seed/factories' +import { host, login } from '../../jest/helpers' const factory = Factory() let clientUser1 let headersUser1 -const mutationFollowUser = (id) => ` +const mutationFollowUser = id => ` mutation { follow(id: "${id}", type: User) } ` -const mutationUnfollowUser = (id) => ` +const mutationUnfollowUser = id => ` mutation { unfollow(id: "${id}", type: User) } @@ -21,12 +21,12 @@ beforeEach(async () => { await factory.create('User', { id: 'u1', email: 'test@example.org', - password: '1234' + password: '1234', }) await factory.create('User', { id: 'u2', email: 'test2@example.org', - password: '1234' + password: '1234', }) headersUser1 = await login({ email: 'test@example.org', password: '1234' }) @@ -43,18 +43,14 @@ describe('follow', () => { it('throws authorization error', async () => { let client client = new GraphQLClient(host) - await expect( - client.request(mutationFollowUser('u2')) - ).rejects.toThrow('Not Authorised') + 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 res = await clientUser1.request(mutationFollowUser('u2')) const expected = { - follow: true + follow: true, } expect(res).toMatchObject(expected) @@ -65,20 +61,16 @@ describe('follow', () => { } }`) const expected2 = { - followedBy: [ - { id: 'u1' } - ], - followedByCurrentUser: true + 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 res = await clientUser1.request(mutationFollowUser('u1')) const expected = { - follow: false + follow: false, } expect(res).toMatchObject(expected) @@ -90,7 +82,7 @@ describe('follow', () => { }`) const expected2 = { followedBy: [], - followedByCurrentUser: false + followedByCurrentUser: false, } expect(User[0]).toMatchObject(expected2) }) @@ -99,26 +91,20 @@ describe('follow', () => { describe('unauthenticated follow', () => { it('throws authorization error', async () => { // follow - await clientUser1.request( - mutationFollowUser('u2') - ) + await clientUser1.request(mutationFollowUser('u2')) // unfollow let client client = new GraphQLClient(host) - await expect( - client.request(mutationUnfollowUser('u2')) - ).rejects.toThrow('Not Authorised') + await expect(client.request(mutationUnfollowUser('u2'))).rejects.toThrow('Not Authorised') }) }) it('I can unfollow a user', async () => { // follow - await clientUser1.request( - mutationFollowUser('u2') - ) + await clientUser1.request(mutationFollowUser('u2')) // unfollow const expected = { - unfollow: true + unfollow: true, } const res = await clientUser1.request(mutationUnfollowUser('u2')) expect(res).toMatchObject(expected) @@ -131,7 +117,7 @@ describe('follow', () => { }`) const expected2 = { followedBy: [], - followedByCurrentUser: false + followedByCurrentUser: false, } expect(User[0]).toMatchObject(expected2) }) diff --git a/backend/src/schema/resolvers/index.js b/backend/src/schema/resolvers/index.js new file mode 100644 index 000000000..3d3a91d68 --- /dev/null +++ b/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/backend/src/resolvers/moderation.js b/backend/src/schema/resolvers/moderation.js similarity index 90% rename from backend/src/resolvers/moderation.js rename to backend/src/schema/resolvers/moderation.js index 7bc1227ff..d61df7545 100644 --- a/backend/src/resolvers/moderation.js +++ b/backend/src/schema/resolvers/moderation.js @@ -14,7 +14,7 @@ export default { const session = driver.session() const res = await session.run(cypher, { id, userId }) session.close() - const [resource] = res.records.map((record) => { + const [resource] = res.records.map(record => { return record.get('resource') }) if (!resource) return null @@ -31,11 +31,11 @@ export default { const session = driver.session() const res = await session.run(cypher, { id }) session.close() - const [resource] = res.records.map((record) => { + const [resource] = res.records.map(record => { return record.get('resource') }) if (!resource) return null return resource.id - } - } + }, + }, } diff --git a/backend/src/resolvers/moderation.spec.js b/backend/src/schema/resolvers/moderation.spec.js similarity index 66% rename from backend/src/resolvers/moderation.spec.js rename to backend/src/schema/resolvers/moderation.spec.js index 28f4dc322..b1dec603b 100644 --- a/backend/src/resolvers/moderation.spec.js +++ b/backend/src/schema/resolvers/moderation.spec.js @@ -1,11 +1,11 @@ -import Factory from '../seed/factories' import { GraphQLClient } from 'graphql-request' -import { host, login } from '../jest/helpers' +import Factory from '../../seed/factories' +import { host, login } from '../../jest/helpers' const factory = Factory() let client -const setupAuthenticateClient = (params) => { +const setupAuthenticateClient = params => { const authenticateClient = async () => { await factory.create('User', params) const headers = await login(params) @@ -46,7 +46,7 @@ describe('disable', () => { beforeEach(() => { // our defaul set of variables variables = { - id: 'blabla' + id: 'blabla', } }) @@ -63,7 +63,7 @@ describe('disable', () => { beforeEach(() => { authenticateClient = setupAuthenticateClient({ email: 'user@example.org', - password: '1234' + password: '1234', }) }) @@ -78,19 +78,17 @@ describe('disable', () => { id: 'u7', email: 'moderator@example.org', password: '1234', - role: 'moderator' + role: 'moderator', }) }) describe('on something that is not a (Comment|Post|User) ', () => { beforeEach(async () => { variables = { - id: 't23' + id: 't23', } createResource = () => { - return Promise.all([ - factory.create('Tag', { id: 't23' }) - ]) + return Promise.all([factory.create('Tag', { id: 't23' })]) } }) @@ -104,21 +102,28 @@ describe('disable', () => { describe('on a comment', () => { beforeEach(async () => { variables = { - id: 'c47' + id: 'c47', } createPostVariables = { id: 'p3', title: 'post to comment on', - content: 'please comment on me' + content: 'please comment on me', } createCommentVariables = { id: 'c47', postId: 'p3', - content: 'this comment was created for this post' + 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 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) } @@ -135,41 +140,39 @@ describe('disable', () => { const expected = { Comment: [{ id: 'c47', disabledBy: { id: 'u7' } }] } await setup() - await expect(client.request( - '{ Comment { id, disabledBy { id } } }' - )).resolves.toEqual(before) + 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) + 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 } ] } + 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 expect(client.request('{ Comment { id disabled } }')).resolves.toEqual(before) await action() - await expect(client.request( - '{ Comment(disabled: true) { id disabled } }' - )).resolves.toEqual(expected) + await expect( + client.request('{ Comment(disabled: true) { id disabled } }'), + ).resolves.toEqual(expected) }) }) describe('on a post', () => { beforeEach(async () => { variables = { - id: 'p9' + 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 + id: 'p9', // that's the ID we will look for }) } }) @@ -185,27 +188,25 @@ describe('disable', () => { const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } await setup() - await expect(client.request( - '{ Post { id, disabledBy { id } } }' - )).resolves.toEqual(before) + 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) + 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 } ] } + 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 expect(client.request('{ Post { id disabled } }')).resolves.toEqual(before) await action() - await expect(client.request( - '{ Post(disabled: true) { id disabled } }' - )).resolves.toEqual(expected) + await expect(client.request('{ Post(disabled: true) { id disabled } }')).resolves.toEqual( + expected, + ) }) }) }) @@ -227,7 +228,7 @@ describe('enable', () => { beforeEach(() => { // our defaul set of variables variables = { - id: 'blabla' + id: 'blabla', } }) @@ -240,7 +241,7 @@ describe('enable', () => { beforeEach(() => { authenticateClient = setupAuthenticateClient({ email: 'user@example.org', - password: '1234' + password: '1234', }) }) @@ -254,20 +255,18 @@ describe('enable', () => { authenticateClient = setupAuthenticateClient({ role: 'moderator', email: 'someUser@example.org', - password: '1234' + password: '1234', }) }) describe('on something that is not a (Comment|Post|User) ', () => { beforeEach(async () => { variables = { - id: 't23' + id: 't23', } createResource = () => { // we cannot create a :DISABLED relationship here - return Promise.all([ - factory.create('Tag', { id: 't23' }) - ]) + return Promise.all([factory.create('Tag', { id: 't23' })]) } }) @@ -281,21 +280,28 @@ describe('enable', () => { describe('on a comment', () => { beforeEach(async () => { variables = { - id: 'c456' + id: 'c456', } createPostVariables = { id: 'p9', title: 'post to comment on', - content: 'please comment on me' + content: 'please comment on me', } createCommentVariables = { id: 'c456', postId: 'p9', - content: 'this comment was created for this post' + 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 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) @@ -319,41 +325,43 @@ describe('enable', () => { const expected = { Comment: [{ id: 'c456', disabledBy: null }] } await setup() - await expect(client.request( - '{ Comment(disabled: true) { id, disabledBy { id } } }' - )).resolves.toEqual(before) + 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) + 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 } ] } + 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 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) + await expect(client.request('{ Comment { id disabled } }')).resolves.toEqual(expected) }) }) describe('on a post', () => { beforeEach(async () => { variables = { - id: 'p9' + id: 'p9', } createResource = async () => { - await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' }) + 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 + id: 'p9', // that's the ID we will look for }) const disableMutation = ` @@ -376,27 +384,25 @@ describe('enable', () => { const expected = { Post: [{ id: 'p9', disabledBy: null }] } await setup() - await expect(client.request( - '{ Post(disabled: true) { id, disabledBy { id } } }' - )).resolves.toEqual(before) + 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) + 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 } ] } + 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 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) + await expect(client.request('{ Post { id disabled } }')).resolves.toEqual(expected) }) }) }) diff --git a/backend/src/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js similarity index 95% rename from backend/src/resolvers/notifications.js rename to backend/src/schema/resolvers/notifications.js index bc3da0acf..ddc1985cf 100644 --- a/backend/src/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -4,11 +4,11 @@ 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/backend/src/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js similarity index 92% rename from backend/src/resolvers/notifications.spec.js rename to backend/src/schema/resolvers/notifications.spec.js index 799bc1594..3876a4be3 100644 --- a/backend/src/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,14 +1,13 @@ - -import Factory from '../seed/factories' import { GraphQLClient } from 'graphql-request' -import { host, login } from '../jest/helpers' +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' + password: '1234', } beforeEach(async () => { @@ -49,12 +48,12 @@ describe('currentUser { notifications }', () => { const neighborParams = { email: 'neighbor@example.org', password: '1234', - id: 'neighbor' + id: 'neighbor', } await Promise.all([ factory.create('User', neighborParams), factory.create('Notification', { id: 'not-for-you' }), - factory.create('Notification', { id: 'already-seen', read: true }) + factory.create('Notification', { id: 'already-seen', read: true }), ]) await factory.create('Notification', { id: 'unseen' }) await factory.authenticateAs(neighborParams) @@ -65,7 +64,7 @@ describe('currentUser { notifications }', () => { 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' }) + factory.relate('Notification', 'Post', { from: 'p1', to: 'already-seen' }), ]) }) @@ -84,10 +83,8 @@ describe('currentUser { notifications }', () => { it('returns only unread notifications of current user', async () => { const expected = { currentUser: { - notifications: [ - { id: 'unseen', post: { id: 'p1' } } - ] - } + notifications: [{ id: 'unseen', post: { id: 'p1' } }], + }, } await expect(client.request(query, variables)).resolves.toEqual(expected) }) @@ -109,9 +106,9 @@ describe('currentUser { notifications }', () => { currentUser: { notifications: [ { id: 'unseen', post: { id: 'p1' } }, - { id: 'already-seen', post: { id: 'p1' } } - ] - } + { id: 'already-seen', post: { id: 'p1' } }, + ], + }, } await expect(client.request(query, variables)).resolves.toEqual(expected) }) @@ -136,7 +133,7 @@ describe('UpdateNotification', () => { id: 'mentioned-1', email: 'mentioned@example.org', password: '1234', - slug: 'mentioned' + slug: 'mentioned', } await factory.create('User', mentionedParams) await factory.create('Notification', { id: 'to-be-updated' }) @@ -144,7 +141,7 @@ describe('UpdateNotification', () => { 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' }) + factory.relate('Notification', 'Post', { from: 'p1', to: 'to-be-updated' }), ]) }) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js new file mode 100644 index 000000000..ea962a662 --- /dev/null +++ b/backend/src/schema/resolvers/posts.js @@ -0,0 +1,30 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' +import fileUpload from './fileUpload' + +export default { + Mutation: { + UpdatePost: async (object, params, context, resolveInfo) => { + params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) + return neo4jgraphql(object, params, context, resolveInfo, false) + }, + + CreatePost: async (object, params, context, resolveInfo) => { + params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) + const result = await neo4jgraphql(object, params, context, resolveInfo, false) + + const session = context.driver.session() + await session.run( + 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' + + 'MERGE (post)<-[:WROTE]-(author) ' + + 'RETURN author', + { + userId: context.user.id, + postId: result.id, + }, + ) + session.close() + + return result + }, + }, +} diff --git a/backend/src/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js similarity index 89% rename from backend/src/resolvers/posts.spec.js rename to backend/src/schema/resolvers/posts.spec.js index 5603683eb..9e2ec70a2 100644 --- a/backend/src/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -1,6 +1,6 @@ -import Factory from '../seed/factories' import { GraphQLClient } from 'graphql-request' -import { host, login } from '../jest/helpers' +import Factory from '../../seed/factories' +import { host, login } from '../../jest/helpers' const factory = Factory() let client @@ -8,7 +8,7 @@ let client beforeEach(async () => { await factory.create('User', { email: 'test@example.org', - password: '1234' + password: '1234', }) }) @@ -47,22 +47,25 @@ describe('CreatePost', () => { const expected = { CreatePost: { title: 'I am a title', - content: 'Some content' - } + content: 'Some content', + }, } await expect(client.request(mutation)).resolves.toMatchObject(expected) }) it('assigns the authenticated user as author', async () => { await client.request(mutation) - const { User } = await client.request(`{ + const { User } = await client.request( + `{ User(email:"test@example.org") { contributions { title } } - }`, { headers }) - expect(User).toEqual([ { contributions: [ { title: 'I am a title' } ] } ]) + }`, + { headers }, + ) + expect(User).toEqual([{ contributions: [{ title: 'I am a title' }] }]) }) describe('disabled and deleted', () => { @@ -86,22 +89,22 @@ describe('UpdatePost', () => { let variables = { id: 'p1', - content: 'New content' + content: 'New content', } beforeEach(async () => { const asAuthor = Factory() await asAuthor.create('User', { email: 'author@example.org', - password: '1234' + password: '1234', }) await asAuthor.authenticateAs({ email: 'author@example.org', - password: '1234' + password: '1234', }) await asAuthor.create('Post', { id: 'p1', - content: 'Old content' + content: 'Old content', }) }) @@ -149,22 +152,22 @@ describe('DeletePost', () => { ` let variables = { - id: 'p1' + id: 'p1', } beforeEach(async () => { const asAuthor = Factory() await asAuthor.create('User', { email: 'author@example.org', - password: '1234' + password: '1234', }) await asAuthor.authenticateAs({ email: 'author@example.org', - password: '1234' + password: '1234', }) await asAuthor.create('Post', { id: 'p1', - content: 'To be deleted' + content: 'To be deleted', }) }) diff --git a/backend/src/resolvers/reports.js b/backend/src/schema/resolvers/reports.js similarity index 68% rename from backend/src/resolvers/reports.js rename to backend/src/schema/resolvers/reports.js index fb912a557..2c0fbfc75 100644 --- a/backend/src/resolvers/reports.js +++ b/backend/src/schema/resolvers/reports.js @@ -7,11 +7,12 @@ export default { const session = driver.session() const reportData = { id: reportId, - createdAt: (new Date()).toISOString(), - description: description + createdAt: new Date().toISOString(), + description: description, } - const res = await session.run(` + const res = await session.run( + ` MATCH (submitter:User {id: $userId}) MATCH (resource {id: $resourceId}) WHERE resource:User OR resource:Comment OR resource:Post @@ -19,11 +20,12 @@ export default { MERGE (resource)<-[:REPORTED]-(report) MERGE (report)<-[:REPORTED]-(submitter) RETURN report, submitter, resource, labels(resource)[0] as type - `, { - resourceId: id, - userId: user.id, - reportData - } + `, + { + resourceId: id, + userId: user.id, + reportData, + }, ) session.close() @@ -32,7 +34,7 @@ export default { report: r.get('report'), submitter: r.get('submitter'), resource: r.get('resource'), - type: r.get('type') + type: r.get('type'), } }) if (!dbResponse) return null @@ -44,20 +46,20 @@ export default { comment: null, user: null, submitter: submitter.properties, - type + type, } switch (type) { - case 'Post': - response.post = resource.properties - break - case 'Comment': - response.comment = resource.properties - break - case 'User': - response.user = resource.properties - break + 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/backend/src/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js similarity index 82% rename from backend/src/resolvers/reports.spec.js rename to backend/src/schema/resolvers/reports.spec.js index 9bd1fe753..6b996b016 100644 --- a/backend/src/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -1,6 +1,6 @@ -import Factory from '../seed/factories' import { GraphQLClient } from 'graphql-request' -import { host, login } from '../jest/helpers' +import Factory from '../../seed/factories' +import { host, login } from '../../jest/helpers' const factory = Factory() @@ -18,13 +18,13 @@ describe('report', () => { await factory.create('User', { id: 'u1', email: 'test@example.org', - password: '1234' + password: '1234', }) await factory.create('User', { id: 'u2', name: 'abusive-user', role: 'user', - email: 'abusive-user@example.org' + email: 'abusive-user@example.org', }) }) @@ -59,7 +59,7 @@ describe('report', () => { describe('invalid resource id', () => { it('returns null', async () => { await expect(action()).resolves.toEqual({ - report: null + report: null, }) }) }) @@ -71,14 +71,14 @@ describe('report', () => { it('creates a report', async () => { await expect(action()).resolves.toEqual({ - report: { description: 'Violates code of conduct' } + report: { description: 'Violates code of conduct' }, }) }) it('returns the submitter', async () => { returnedObject = '{ submitter { email } }' await expect(action()).resolves.toEqual({ - report: { submitter: { email: 'test@example.org' } } + report: { submitter: { email: 'test@example.org' } }, }) }) @@ -86,14 +86,14 @@ describe('report', () => { it('returns type "User"', async () => { returnedObject = '{ type }' await expect(action()).resolves.toEqual({ - report: { type: 'User' } + report: { type: 'User' }, }) }) it('returns resource in user attribute', async () => { returnedObject = '{ user { name } }' await expect(action()).resolves.toEqual({ - report: { user: { name: 'abusive-user' } } + report: { user: { name: 'abusive-user' } }, }) }) }) @@ -101,28 +101,31 @@ describe('report', () => { 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' }) + 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' } + 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' } } + 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 } + report: { user: null }, }) }) }) @@ -132,25 +135,32 @@ describe('report', () => { createPostVariables = { id: 'p1', title: 'post to comment on', - content: 'please comment on me' + content: 'please comment on me', } - const asAuthenticatedUser = await factory.authenticateAs({ email: 'test@example.org', password: '1234' }) + 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.' }) + 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' } + report: { type: 'Comment' }, }) }) it('returns resource in comment attribute', async () => { returnedObject = '{ comment { content } }' await expect(action()).resolves.toEqual({ - report: { comment: { content: 'Robert getting tired.' } } + report: { comment: { content: 'Robert getting tired.' } }, }) }) }) diff --git a/backend/src/resolvers/rewards.js b/backend/src/schema/resolvers/rewards.js similarity index 92% rename from backend/src/resolvers/rewards.js rename to backend/src/schema/resolvers/rewards.js index a7a8c1ab7..ec5043da3 100644 --- a/backend/src/resolvers/rewards.js +++ b/backend/src/schema/resolvers/rewards.js @@ -10,8 +10,8 @@ export default { RETURN rewardedUser {.id}`, { badgeId: fromBadgeId, - rewardedUserId: toUserId - } + rewardedUserId: toUserId, + }, ) const [rewardedUser] = transactionRes.records.map(record => { @@ -33,8 +33,8 @@ export default { RETURN rewardedUser {.id}`, { badgeId: fromBadgeId, - rewardedUserId: toUserId - } + rewardedUserId: toUserId, + }, ) const [rewardedUser] = transactionRes.records.map(record => { return record.get('rewardedUser') @@ -42,6 +42,6 @@ export default { session.close() return rewardedUser.id - } - } + }, + }, } diff --git a/backend/src/resolvers/rewards.spec.js b/backend/src/schema/resolvers/rewards.spec.js similarity index 72% rename from backend/src/resolvers/rewards.spec.js rename to backend/src/schema/resolvers/rewards.spec.js index 567228eca..2bdd9a39b 100644 --- a/backend/src/resolvers/rewards.spec.js +++ b/backend/src/schema/resolvers/rewards.spec.js @@ -1,6 +1,6 @@ -import Factory from '../seed/factories' import { GraphQLClient } from 'graphql-request' -import { host, login } from '../jest/helpers' +import Factory from '../../seed/factories' +import { host, login } from '../../jest/helpers' const factory = Factory() @@ -10,24 +10,24 @@ describe('rewards', () => { id: 'u1', role: 'user', email: 'user@example.org', - password: '1234' + password: '1234', }) await factory.create('User', { id: 'u2', role: 'moderator', - email: 'moderator@example.org' + email: 'moderator@example.org', }) await factory.create('User', { id: 'u3', role: 'admin', - email: 'admin@example.org' + 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' + icon: '/img/badges/indiegogo_en_rhino.svg', }) }) @@ -48,15 +48,13 @@ describe('rewards', () => { describe('unauthenticated', () => { const variables = { from: 'b6', - to: 'u1' + to: 'u1', } let client it('throws authorization error', async () => { client = new GraphQLClient(host) - await expect( - client.request(mutation, variables) - ).rejects.toThrow('Not Authorised') + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') }) }) @@ -70,14 +68,12 @@ describe('rewards', () => { it('rewards a badge to user', async () => { const variables = { from: 'b6', - to: 'u1' + to: 'u1', } const expected = { - reward: 'u1' + reward: 'u1', } - await expect( - client.request(mutation, variables) - ).resolves.toEqual(expected) + await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) it('rewards a second different badge to same user', async () => { await factory.create('Badge', { @@ -85,41 +81,37 @@ describe('rewards', () => { key: 'indiegogo_en_racoon', type: 'crowdfunding', status: 'permanent', - icon: '/img/badges/indiegogo_en_racoon.svg' + icon: '/img/badges/indiegogo_en_racoon.svg', }) const variables = { from: 'b1', - to: 'u1' + to: 'u1', } const expected = { - reward: 'u1' + reward: 'u1', } - await expect( - client.request(mutation, variables) - ).resolves.toEqual(expected) + 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' + to: 'u1', } await client.request(mutation, variables1) const variables2 = { from: 'b6', - to: 'u2' + to: 'u2', } const expected = { - reward: 'u2' + reward: 'u2', } - await expect( - client.request(mutation, variables2) - ).resolves.toEqual(expected) + 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' + to: 'u1', } await client.request(mutation, variables) await client.request(mutation, variables) @@ -132,16 +124,14 @@ describe('rewards', () => { ` const expected = { User: [{ badgesCount: 1 }] } - await expect( - client.request(query) - ).resolves.toEqual(expected) + await expect(client.request(query)).resolves.toEqual(expected) }) }) describe('authenticated moderator', () => { const variables = { from: 'b6', - to: 'u1' + to: 'u1', } let client beforeEach(async () => { @@ -151,9 +141,7 @@ describe('rewards', () => { describe('rewards bage to user', () => { it('throws authorization error', async () => { - await expect( - client.request(mutation, variables) - ).rejects.toThrow('Not Authorised') + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') }) }) }) @@ -165,10 +153,10 @@ describe('rewards', () => { }) const variables = { from: 'b6', - to: 'u1' + to: 'u1', } const expected = { - unreward: 'u1' + unreward: 'u1', } const mutation = ` @@ -185,9 +173,7 @@ describe('rewards', () => { it('throws authorization error', async () => { client = new GraphQLClient(host) - await expect( - client.request(mutation, variables) - ).rejects.toThrow('Not Authorised') + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') }) }) @@ -199,17 +185,15 @@ describe('rewards', () => { }) it('removes a badge from user', async () => { - await expect( - client.request(mutation, variables) - ).resolves.toEqual(expected) + 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') + await expect(client.request(mutation, variables)).rejects.toThrow( + "Cannot read property 'id' of undefined", + ) }) }) @@ -222,9 +206,7 @@ describe('rewards', () => { describe('removes bage from user', () => { it('throws authorization error', async () => { - await expect( - client.request(mutation, variables) - ).rejects.toThrow('Not Authorised') + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') }) }) }) diff --git a/backend/src/resolvers/shout.js b/backend/src/schema/resolvers/shout.js similarity index 92% rename from backend/src/resolvers/shout.js rename to backend/src/schema/resolvers/shout.js index 69c39a3a9..d2d7f652e 100644 --- a/backend/src/resolvers/shout.js +++ b/backend/src/schema/resolvers/shout.js @@ -12,8 +12,8 @@ export default { { id, type, - userId: context.user.id - } + userId: context.user.id, + }, ) const [isShouted] = transactionRes.records.map(record => { @@ -37,8 +37,8 @@ export default { { id, type, - userId: context.user.id - } + userId: context.user.id, + }, ) const [isShouted] = transactionRes.records.map(record => { return record.get('isShouted') @@ -46,6 +46,6 @@ export default { session.close() return isShouted - } - } + }, + }, } diff --git a/backend/src/resolvers/shout.spec.js b/backend/src/schema/resolvers/shout.spec.js similarity index 75% rename from backend/src/resolvers/shout.spec.js rename to backend/src/schema/resolvers/shout.spec.js index 88866a74f..a94f7ca0b 100644 --- a/backend/src/resolvers/shout.spec.js +++ b/backend/src/schema/resolvers/shout.spec.js @@ -1,17 +1,17 @@ -import Factory from '../seed/factories' import { GraphQLClient } from 'graphql-request' -import { host, login } from '../jest/helpers' +import Factory from '../../seed/factories' +import { host, login } from '../../jest/helpers' const factory = Factory() let clientUser1, clientUser2 let headersUser1, headersUser2 -const mutationShoutPost = (id) => ` +const mutationShoutPost = id => ` mutation { shout(id: "${id}", type: Post) } ` -const mutationUnshoutPost = (id) => ` +const mutationUnshoutPost = id => ` mutation { unshout(id: "${id}", type: Post) } @@ -21,12 +21,12 @@ beforeEach(async () => { await factory.create('User', { id: 'u1', email: 'test@example.org', - password: '1234' + password: '1234', }) await factory.create('User', { id: 'u2', email: 'test2@example.org', - password: '1234' + password: '1234', }) headersUser1 = await login({ email: 'test@example.org', password: '1234' }) @@ -62,18 +62,14 @@ describe('shout', () => { it('throws authorization error', async () => { let client client = new GraphQLClient(host) - await expect( - client.request(mutationShoutPost('p1')) - ).rejects.toThrow('Not Authorised') + 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 res = await clientUser1.request(mutationShoutPost('p2')) const expected = { - shout: true + shout: true, } expect(res).toMatchObject(expected) @@ -83,17 +79,15 @@ describe('shout', () => { } }`) const expected2 = { - shoutedByCurrentUser: true + shoutedByCurrentUser: true, } expect(Post[0]).toMatchObject(expected2) }) it('I can`t shout my own post', async () => { - const res = await clientUser1.request( - mutationShoutPost('p1') - ) + const res = await clientUser1.request(mutationShoutPost('p1')) const expected = { - shout: false + shout: false, } expect(res).toMatchObject(expected) @@ -103,7 +97,7 @@ describe('shout', () => { } }`) const expected2 = { - shoutedByCurrentUser: false + shoutedByCurrentUser: false, } expect(Post[0]).toMatchObject(expected2) }) @@ -113,25 +107,19 @@ describe('shout', () => { describe('unauthenticated shout', () => { it('throws authorization error', async () => { // shout - await clientUser1.request( - mutationShoutPost('p2') - ) + await clientUser1.request(mutationShoutPost('p2')) // unshout let client client = new GraphQLClient(host) - await expect( - client.request(mutationUnshoutPost('p2')) - ).rejects.toThrow('Not Authorised') + 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') - ) + await clientUser1.request(mutationShoutPost('p2')) const expected = { - unshout: true + unshout: true, } // unshout const res = await clientUser1.request(mutationUnshoutPost('p2')) @@ -143,7 +131,7 @@ describe('shout', () => { } }`) const expected2 = { - shoutedByCurrentUser: false + shoutedByCurrentUser: false, } expect(Post[0]).toMatchObject(expected2) }) diff --git a/backend/src/resolvers/socialMedia.js b/backend/src/schema/resolvers/socialMedia.js similarity index 88% rename from backend/src/resolvers/socialMedia.js rename to backend/src/schema/resolvers/socialMedia.js index ef143a478..0bc03ea74 100644 --- a/backend/src/resolvers/socialMedia.js +++ b/backend/src/schema/resolvers/socialMedia.js @@ -5,16 +5,17 @@ export default { 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`, { + RETURN owner`, + { userId: context.user.id, - socialMediaId: socialMedia.id - } + socialMediaId: socialMedia.id, + }, ) session.close() @@ -24,6 +25,6 @@ export default { const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false) return socialMedia - } - } + }, + }, } diff --git a/backend/src/schema/resolvers/socialMedia.spec.js b/backend/src/schema/resolvers/socialMedia.spec.js new file mode 100644 index 000000000..38850761c --- /dev/null +++ b/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/backend/src/schema/resolvers/statistics.js b/backend/src/schema/resolvers/statistics.js new file mode 100644 index 000000000..f09b7219d --- /dev/null +++ b/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/backend/src/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js similarity index 80% rename from backend/src/resolvers/user_management.js rename to backend/src/schema/resolvers/user_management.js index 26dfb81db..eb07a07b3 100644 --- a/backend/src/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -1,4 +1,4 @@ -import encode from '../jwt/encode' +import encode from '../../jwt/encode' import bcrypt from 'bcryptjs' import { AuthenticationError } from 'apollo-server' import { neo4jgraphql } from 'neo4j-graphql-js' @@ -12,7 +12,7 @@ export default { const { user } = ctx if (!user) return null return neo4jgraphql(object, { id: user.id }, ctx, resolveInfo, false) - } + }, }, Mutation: { signup: async (parent, { email, password }, { req }) => { @@ -34,12 +34,12 @@ export default { 'MATCH (user:User {email: $userEmail}) ' + 'RETURN user {.id, .slug, .name, .avatar, .email, .password, .role, .disabled} as user LIMIT 1', { - userEmail: email - } + userEmail: email, + }, ) session.close() - const [currentUser] = await result.records.map(function (record) { + const [currentUser] = await result.records.map(function(record) { return record.get('user') }) @@ -50,29 +50,23 @@ export default { ) { delete currentUser.password return encode(currentUser) - } else if (currentUser && - currentUser.disabled - ) { + } 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 } - ) => { + 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 - } + userEmail: user.email, + }, ) - const [currentUser] = result.records.map(function (record) { + const [currentUser] = result.records.map(function(record) { return record.get('user') }) @@ -81,9 +75,7 @@ export default { } if (await bcrypt.compareSync(newPassword, currentUser.password)) { - throw new AuthenticationError( - 'Old password and new password should be different' - ) + throw new AuthenticationError('Old password and new password should be different') } else { const newHashedPassword = await bcrypt.hashSync(newPassword, 10) session.run( @@ -93,13 +85,13 @@ export default { `, { userEmail: user.email, - newHashedPassword - } + newHashedPassword, + }, ) session.close() return encode(currentUser) } - } - } + }, + }, } diff --git a/backend/src/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js similarity index 84% rename from backend/src/resolvers/user_management.spec.js rename to backend/src/schema/resolvers/user_management.spec.js index 7c0be08f3..cf648a6bd 100644 --- a/backend/src/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -1,8 +1,9 @@ import gql from 'graphql-tag' -import Factory from '../seed/factories' import { GraphQLClient, request } from 'graphql-request' import jwt from 'jsonwebtoken' -import { host, login } from '../jest/helpers' +import CONFIG from './../../config' +import Factory from '../../seed/factories' +import { host, login } from '../../jest/helpers' const factory = Factory() @@ -24,10 +25,10 @@ const factory = Factory() // } const jennyRostocksHeaders = { authorization: - 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc' + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc', } -const disable = async (id) => { +const disable = async id => { const moderatorParams = { email: 'moderator@example.org', role: 'moderator', password: '1234' } const asModerator = Factory() await asModerator.create('User', moderatorParams) @@ -43,7 +44,7 @@ beforeEach(async () => { slug: 'matilde-hermiston', role: 'user', email: 'test@example.org', - password: '1234' + password: '1234', }) }) @@ -56,7 +57,7 @@ describe('isLoggedIn', () => { describe('unauthenticated', () => { it('returns false', async () => { await expect(request(host, query)).resolves.toEqual({ - isLoggedIn: false + isLoggedIn: false, }) }) }) @@ -67,7 +68,7 @@ describe('isLoggedIn', () => { it('returns false', async () => { await expect(client.request(query)).resolves.toEqual({ - isLoggedIn: false + isLoggedIn: false, }) }) }) @@ -77,7 +78,7 @@ describe('isLoggedIn', () => { it('returns false', async () => { await expect(client.request(query)).resolves.toEqual({ - isLoggedIn: false + isLoggedIn: false, }) }) @@ -87,7 +88,7 @@ describe('isLoggedIn', () => { // see the decoded token above await factory.create('User', { id: 'u3' }) await expect(client.request(query)).resolves.toEqual({ - isLoggedIn: true + isLoggedIn: true, }) }) }) @@ -100,7 +101,7 @@ describe('isLoggedIn', () => { it('returns false', async () => { await expect(client.request(query)).resolves.toEqual({ - isLoggedIn: false + isLoggedIn: false, }) }) }) @@ -156,8 +157,8 @@ describe('currentUser', () => { id: 'acb2d923-f3af-479e-9f00-61b12e864666', name: 'Matilde Hermiston', slug: 'matilde-hermiston', - role: 'user' - } + role: 'user', + }, } await expect(client.request(query)).resolves.toEqual(expected) }) @@ -181,11 +182,11 @@ describe('login', () => { host, mutation({ email: 'test@example.org', - password: '1234' - }) + password: '1234', + }), ) const token = data.login - jwt.verify(token, process.env.JWT_SECRET, (err, data) => { + jwt.verify(token, CONFIG.JWT_SECRET, (err, data) => { expect(data.email).toEqual('test@example.org') expect(err).toBeNull() }) @@ -200,9 +201,9 @@ describe('login', () => { host, mutation({ email: 'test@example.org', - password: '1234' - }) - ) + password: '1234', + }), + ), ).rejects.toThrow('Your account has been disabled.') }) }) @@ -214,9 +215,9 @@ describe('login', () => { host, mutation({ email: 'test@example.org', - password: 'wrong' - }) - ) + password: 'wrong', + }), + ), ).rejects.toThrow('Incorrect email address or password.') }) }) @@ -228,9 +229,9 @@ describe('login', () => { host, mutation({ email: 'non-existent@example.org', - password: 'wrong' - }) - ) + password: 'wrong', + }), + ), ).rejects.toThrow('Incorrect email address or password.') }) }) @@ -261,9 +262,9 @@ describe('change password', () => { host, mutation({ oldPassword: '1234', - newPassword: '1234' - }) - ) + newPassword: '1234', + }), + ), ).rejects.toThrow('Not Authorised!') }) }) @@ -274,9 +275,9 @@ describe('change password', () => { client.request( mutation({ oldPassword: '1234', - newPassword: '1234' - }) - ) + newPassword: '1234', + }), + ), ).rejects.toThrow('Old password and new password should be different') }) }) @@ -287,9 +288,9 @@ describe('change password', () => { client.request( mutation({ oldPassword: 'notOldPassword', - newPassword: '12345' - }) - ) + newPassword: '12345', + }), + ), ).rejects.toThrow('Old password is not correct') }) }) @@ -299,14 +300,14 @@ describe('change password', () => { let response = await client.request( mutation({ oldPassword: '1234', - newPassword: '12345' - }) + newPassword: '12345', + }), + ) + await expect(response).toEqual( + expect.objectContaining({ + changePassword: expect.any(String), + }), ) - await expect( - response - ).toEqual(expect.objectContaining({ - changePassword: expect.any(String) - })) }) }) }) @@ -320,14 +321,16 @@ describe('do not expose private RSA key', () => { id publicKey } - }` + } + ` const queryUserPrivateKey = gql` query($queriedUserSlug: String) { User(slug: $queriedUserSlug) { id privateKey } - }` + } + ` const actionGenUserWithKeys = async () => { // Generate user with "privateKey" via 'CreateUser' mutation instead of using the factories "factory.create('User', {...})", see above. @@ -336,14 +339,17 @@ describe('do not expose private RSA key', () => { password: 'xYz', slug: 'apfel-strudel', name: 'Apfel Strudel', - email: 'apfel-strudel@test.org' + email: 'apfel-strudel@test.org', } - await client.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 + await client.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 + `, + variables, ) } @@ -356,13 +362,17 @@ describe('do not expose private RSA key', () => { it('returns publicKey', async () => { await actionGenUserWithKeys() await expect( - await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }) - ).toEqual(expect.objectContaining({ - User: [{ - id: 'bcb2d923-f3af-479e-9f00-61b12e864667', - publicKey: expect.any(String) - }] - })) + await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }), + ).toEqual( + expect.objectContaining({ + User: [ + { + id: 'bcb2d923-f3af-479e-9f00-61b12e864667', + publicKey: expect.any(String), + }, + ], + }), + ) }) }) @@ -370,7 +380,7 @@ describe('do not expose private RSA key', () => { it('throws "Not Authorised!"', async () => { await actionGenUserWithKeys() await expect( - client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }) + client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }), ).rejects.toThrow('Not Authorised') }) }) @@ -385,13 +395,17 @@ describe('do not expose private RSA key', () => { it('returns publicKey', async () => { await actionGenUserWithKeys() await expect( - await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }) - ).toEqual(expect.objectContaining({ - User: [{ - id: 'bcb2d923-f3af-479e-9f00-61b12e864667', - publicKey: expect.any(String) - }] - })) + await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }), + ).toEqual( + expect.objectContaining({ + User: [ + { + id: 'bcb2d923-f3af-479e-9f00-61b12e864667', + publicKey: expect.any(String), + }, + ], + }), + ) }) }) @@ -399,7 +413,7 @@ describe('do not expose private RSA key', () => { it('throws "Not Authorised!"', async () => { await actionGenUserWithKeys() await expect( - client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }) + client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }), ).rejects.toThrow('Not Authorised') }) }) diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js new file mode 100644 index 000000000..53bf0967e --- /dev/null +++ b/backend/src/schema/resolvers/users.js @@ -0,0 +1,15 @@ +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) + }, + }, +} diff --git a/backend/src/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js similarity index 70% rename from backend/src/resolvers/users.spec.js rename to backend/src/schema/resolvers/users.spec.js index 48e4741d7..a5c50f4f9 100644 --- a/backend/src/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -1,6 +1,6 @@ import { GraphQLClient } from 'graphql-request' -import { host } from '../jest/helpers' -import Factory from '../seed/factories' +import { host } from '../../jest/helpers' +import Factory from '../../seed/factories' const factory = Factory() let client @@ -24,15 +24,14 @@ describe('users', () => { const variables = { name: 'John Doe', password: '123', - email: '123@123.de' + email: '123@123.de', } const expected = { CreateUser: { - id: expect.any(String) - } + id: expect.any(String), + }, } - await expect(client.request(mutation, variables)) - .resolves.toEqual(expected) + await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) }) @@ -54,35 +53,33 @@ describe('users', () => { it('name within specifications', async () => { const variables = { id: 'u47', - name: 'James Doe' + name: 'James Doe', } const expected = { UpdateUser: { id: 'u47', - name: 'James Doe' - } + name: 'James Doe', + }, } - await expect(client.request(mutation, variables)) - .resolves.toEqual(expected) + await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) it('with no name', async () => { const variables = { - id: 'u47' + id: 'u47', + name: null, } const expected = 'Username must be at least 3 characters long!' - await expect(client.request(mutation, variables)) - .rejects.toThrow(expected) + await expect(client.request(mutation, variables)).rejects.toThrow(expected) }) it('with too short name', async () => { const variables = { id: 'u47', - name: ' ' + name: ' ', } const expected = 'Username must be at least 3 characters long!' - await expect(client.request(mutation, variables)) - .rejects.toThrow(expected) + await expect(client.request(mutation, variables)).rejects.toThrow(expected) }) }) }) diff --git a/backend/src/schema/types/enum/BadgeStatus.gql b/backend/src/schema/types/enum/BadgeStatus.gql new file mode 100644 index 000000000..b109663b3 --- /dev/null +++ b/backend/src/schema/types/enum/BadgeStatus.gql @@ -0,0 +1,4 @@ +enum BadgeStatus { + permanent + temporary +} \ No newline at end of file diff --git a/backend/src/schema/types/enum/BadgeType.gql b/backend/src/schema/types/enum/BadgeType.gql new file mode 100644 index 000000000..eccf2e661 --- /dev/null +++ b/backend/src/schema/types/enum/BadgeType.gql @@ -0,0 +1,4 @@ +enum BadgeType { + role + crowdfunding +} \ No newline at end of file diff --git a/backend/src/schema/types/enum/UserGroup.gql b/backend/src/schema/types/enum/UserGroup.gql new file mode 100644 index 000000000..af25bcc69 --- /dev/null +++ b/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/backend/src/schema/types/enum/Visibility.gql b/backend/src/schema/types/enum/Visibility.gql new file mode 100644 index 000000000..4f9d5591a --- /dev/null +++ b/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/backend/src/schema/types/index.js b/backend/src/schema/types/index.js new file mode 100644 index 000000000..bcdceed44 --- /dev/null +++ b/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/backend/src/schema/types/scalar/Date.gql_ b/backend/src/schema/types/scalar/Date.gql_ new file mode 100644 index 000000000..7b0004ea3 --- /dev/null +++ b/backend/src/schema/types/scalar/Date.gql_ @@ -0,0 +1 @@ +scalar Date \ No newline at end of file diff --git a/backend/src/schema/types/scalar/DateTime.gql_ b/backend/src/schema/types/scalar/DateTime.gql_ new file mode 100644 index 000000000..af973932f --- /dev/null +++ b/backend/src/schema/types/scalar/DateTime.gql_ @@ -0,0 +1 @@ +scalar DateTime \ No newline at end of file diff --git a/backend/src/schema/types/scalar/Time.gql_ b/backend/src/schema/types/scalar/Time.gql_ new file mode 100644 index 000000000..53becdd66 --- /dev/null +++ b/backend/src/schema/types/scalar/Time.gql_ @@ -0,0 +1 @@ +scalar Time \ No newline at end of file diff --git a/backend/src/schema/types/scalar/Upload.gql b/backend/src/schema/types/scalar/Upload.gql new file mode 100644 index 000000000..fca9ea1fc --- /dev/null +++ b/backend/src/schema/types/scalar/Upload.gql @@ -0,0 +1 @@ +scalar Upload \ No newline at end of file diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql new file mode 100644 index 000000000..ab8b25399 --- /dev/null +++ b/backend/src/schema/types/schema.gql @@ -0,0 +1,134 @@ +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 +} + +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 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/backend/src/schema.graphql b/backend/src/schema/types/schema_full.gql_ similarity index 99% rename from backend/src/schema.graphql rename to backend/src/schema/types/schema_full.gql_ index 902a7abf9..a581d287c 100644 --- a/backend/src/schema.graphql +++ b/backend/src/schema/types/schema_full.gql_ @@ -1,3 +1,5 @@ +scalar Upload + type Query { isLoggedIn: Boolean! # Get the currently logged in User based on the given JWT Token @@ -18,6 +20,7 @@ type Query { ) CommentByPost(postId: ID!): [Comment]! } + type Mutation { # Get a JWT Token for the given Email and password login(email: String!, password: String!): String! @@ -99,6 +102,7 @@ type User { slug: String password: String! avatar: String + avatarUpload: Upload deleted: Boolean disabled: Boolean disabledBy: User @relation(name: "DISABLED", direction: "IN") @@ -175,6 +179,7 @@ type Post { content: String! contentExcerpt: String image: String + imageUpload: Upload visibility: VisibilityEnum deleted: Boolean disabled: Boolean diff --git a/backend/src/schema/types/type/Badge.gql b/backend/src/schema/types/type/Badge.gql new file mode 100644 index 000000000..68c5d5707 --- /dev/null +++ b/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/backend/src/schema/types/type/Category.gql b/backend/src/schema/types/type/Category.gql new file mode 100644 index 000000000..5920ebbdb --- /dev/null +++ b/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/backend/src/schema/types/type/Comment.gql b/backend/src/schema/types/type/Comment.gql new file mode 100644 index 000000000..077366e8a --- /dev/null +++ b/backend/src/schema/types/type/Comment.gql @@ -0,0 +1,14 @@ +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") +} \ No newline at end of file diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql new file mode 100644 index 000000000..c402a1233 --- /dev/null +++ b/backend/src/schema/types/type/Post.gql @@ -0,0 +1,43 @@ +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 + + 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 + """ + ) +} diff --git a/backend/src/schema/types/type/Tag.gql b/backend/src/schema/types/type/Tag.gql new file mode 100644 index 000000000..ecbd0b46a --- /dev/null +++ b/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/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql new file mode 100644 index 000000000..1287aa45f --- /dev/null +++ b/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 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)") +} \ No newline at end of file diff --git a/backend/src/seed/factories/badges.js b/backend/src/seed/factories/badges.js index 6f5f8d69a..6414e9f36 100644 --- a/backend/src/seed/factories/badges.js +++ b/backend/src/seed/factories/badges.js @@ -1,12 +1,12 @@ import uuid from 'uuid/v4' -export default function (params) { +export default function(params) { const { id = uuid(), key = '', type = 'crowdfunding', status = 'permanent', - icon = '/img/badges/indiegogo_en_panda.svg' + icon = '/img/badges/indiegogo_en_panda.svg', } = params return { @@ -14,8 +14,8 @@ export default function (params) { mutation( $id: ID $key: String! - $type: BadgeTypeEnum! - $status: BadgeStatusEnum! + $type: BadgeType! + $status: BadgeStatus! $icon: String! ) { CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) { @@ -23,6 +23,6 @@ export default function (params) { } } `, - variables: { id, key, type, status, icon } + variables: { id, key, type, status, icon }, } } diff --git a/backend/src/seed/factories/categories.js b/backend/src/seed/factories/categories.js index 5c1b3ce10..341f1b1fd 100644 --- a/backend/src/seed/factories/categories.js +++ b/backend/src/seed/factories/categories.js @@ -1,12 +1,7 @@ import uuid from 'uuid/v4' -export default function (params) { - const { - id = uuid(), - name, - slug, - icon - } = params +export default function(params) { + const { id = uuid(), name, slug, icon } = params return { mutation: ` @@ -17,6 +12,6 @@ export default function (params) { } } `, - variables: { id, name, slug, icon } + variables: { id, name, slug, icon }, } } diff --git a/backend/src/seed/factories/comments.js b/backend/src/seed/factories/comments.js index ba3a85840..b1079e392 100644 --- a/backend/src/seed/factories/comments.js +++ b/backend/src/seed/factories/comments.js @@ -1,14 +1,11 @@ import faker from 'faker' import uuid from 'uuid/v4' -export default function (params) { +export default function(params) { const { id = uuid(), postId = 'p6', - content = [ - faker.lorem.sentence(), - faker.lorem.sentence() - ].join('. ') + content = [faker.lorem.sentence(), faker.lorem.sentence()].join('. '), } = params return { @@ -19,6 +16,6 @@ export default function (params) { } } `, - variables: { id, postId, content } + variables: { id, postId, content }, } } diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index a0cb310ab..211edf87e 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -19,7 +19,7 @@ const authenticatedHeaders = async ({ email, password }, host) => { }` const response = await request(host, mutation) return { - authorization: `Bearer ${response.login}` + authorization: `Bearer ${response.login}`, } } const factories = { @@ -31,7 +31,7 @@ const factories = { Category: createCategory, Tag: createTag, Report: createReport, - Notification: createNotification + Notification: createNotification, } export const cleanDatabase = async (options = {}) => { @@ -47,11 +47,8 @@ export const cleanDatabase = async (options = {}) => { } } -export default function Factory (options = {}) { - const { - neo4jDriver = getDriver(), - seedServerHost = 'http://127.0.0.1:4001' - } = options +export default function Factory(options = {}) { + const { neo4jDriver = getDriver(), seedServerHost = 'http://127.0.0.1:4001' } = options const graphQLClient = new GraphQLClient(seedServerHost) @@ -61,21 +58,18 @@ export default function Factory (options = {}) { graphQLClient, factories, lastResponse: null, - async authenticateAs ({ email, password }) { - const headers = await authenticatedHeaders( - { email, password }, - seedServerHost - ) + 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) { + 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) { + async relate(node, relationship, properties) { const { from, to } = properties const mutation = ` mutation { @@ -88,11 +82,11 @@ export default function Factory (options = {}) { this.lastResponse = await this.graphQLClient.request(mutation) return this }, - async mutate (mutation, variables) { + async mutate(mutation, variables) { this.lastResponse = await this.graphQLClient.request(mutation, variables) return this }, - async shout (properties) { + async shout(properties) { const { id, type } = properties const mutation = ` mutation { @@ -105,7 +99,7 @@ export default function Factory (options = {}) { this.lastResponse = await this.graphQLClient.request(mutation) return this }, - async follow (properties) { + async follow(properties) { const { id, type } = properties const mutation = ` mutation { @@ -118,10 +112,10 @@ export default function Factory (options = {}) { this.lastResponse = await this.graphQLClient.request(mutation) return this }, - async cleanDatabase () { + async cleanDatabase() { this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) return this - } + }, } result.authenticateAs.bind(result) result.create.bind(result) diff --git a/backend/src/seed/factories/notifications.js b/backend/src/seed/factories/notifications.js index f7797200f..d14d4294a 100644 --- a/backend/src/seed/factories/notifications.js +++ b/backend/src/seed/factories/notifications.js @@ -1,10 +1,7 @@ import uuid from 'uuid/v4' -export default function (params) { - const { - id = uuid(), - read = false - } = params +export default function(params) { + const { id = uuid(), read = false } = params return { mutation: ` @@ -15,6 +12,6 @@ export default function (params) { } } `, - variables: { id, read } + variables: { id, read }, } } diff --git a/backend/src/seed/factories/organizations.js b/backend/src/seed/factories/organizations.js index dd4100b26..536de1597 100644 --- a/backend/src/seed/factories/organizations.js +++ b/backend/src/seed/factories/organizations.js @@ -1,11 +1,11 @@ import faker from 'faker' import uuid from 'uuid/v4' -export default function create (params) { +export default function create(params) { const { id = uuid(), name = faker.company.companyName(), - description = faker.company.catchPhrase() + description = faker.company.catchPhrase(), } = params return { @@ -16,6 +16,6 @@ export default function create (params) { } } `, - variables: { id, name, description } + variables: { id, name, description }, } } diff --git a/backend/src/seed/factories/posts.js b/backend/src/seed/factories/posts.js index cbc73dbf8..ea92f7d9f 100644 --- a/backend/src/seed/factories/posts.js +++ b/backend/src/seed/factories/posts.js @@ -1,7 +1,7 @@ import faker from 'faker' import uuid from 'uuid/v4' -export default function (params) { +export default function(params) { const { id = uuid(), slug = '', @@ -11,11 +11,11 @@ export default function (params) { faker.lorem.sentence(), faker.lorem.sentence(), faker.lorem.sentence(), - faker.lorem.sentence() + faker.lorem.sentence(), ].join('. '), image = faker.image.image(), visibility = 'public', - deleted = false + deleted = false, } = params return { @@ -26,7 +26,7 @@ export default function (params) { $title: String! $content: String! $image: String - $visibility: VisibilityEnum + $visibility: Visibility $deleted: Boolean ) { CreatePost( @@ -43,6 +43,6 @@ export default function (params) { } } `, - variables: { id, slug, title, content, image, visibility, deleted } + variables: { id, slug, title, content, image, visibility, deleted }, } } diff --git a/backend/src/seed/factories/reports.js b/backend/src/seed/factories/reports.js index 130c20c37..40d0e6179 100644 --- a/backend/src/seed/factories/reports.js +++ b/backend/src/seed/factories/reports.js @@ -1,10 +1,7 @@ import faker from 'faker' -export default function create (params) { - const { - description = faker.lorem.sentence(), - id - } = params +export default function create(params) { + const { description = faker.lorem.sentence(), id } = params return { mutation: ` @@ -15,6 +12,6 @@ export default function create (params) { } } `, - variables: { id, description } + variables: { id, description }, } } diff --git a/backend/src/seed/factories/tags.js b/backend/src/seed/factories/tags.js index 558b68957..15ded1986 100644 --- a/backend/src/seed/factories/tags.js +++ b/backend/src/seed/factories/tags.js @@ -1,10 +1,7 @@ import uuid from 'uuid/v4' -export default function (params) { - const { - id = uuid(), - name = '#human-connection' - } = params +export default function(params) { + const { id = uuid(), name = '#human-connection' } = params return { mutation: ` @@ -14,6 +11,6 @@ export default function (params) { } } `, - variables: { id, name } + variables: { id, name }, } } diff --git a/backend/src/seed/factories/users.js b/backend/src/seed/factories/users.js index a088b4c54..ca17d1721 100644 --- a/backend/src/seed/factories/users.js +++ b/backend/src/seed/factories/users.js @@ -1,7 +1,7 @@ import faker from 'faker' import uuid from 'uuid/v4' -export default function create (params) { +export default function create(params) { const { id = uuid(), name = faker.name.findName(), @@ -10,7 +10,7 @@ export default function create (params) { password = '1234', role = 'user', avatar = faker.internet.avatar(), - about = faker.lorem.paragraph() + about = faker.lorem.paragraph(), } = params return { @@ -23,7 +23,7 @@ export default function create (params) { $email: String! $avatar: String $about: String - $role: UserGroupEnum + $role: UserGroup ) { CreateUser( id: $id @@ -46,6 +46,6 @@ export default function create (params) { } } `, - variables: { id, name, slug, password, email, avatar, about, role } + variables: { id, name, slug, password, email, avatar, about, role }, } } diff --git a/backend/src/seed/reset-db.js b/backend/src/seed/reset-db.js index 4075489f9..125d135d8 100644 --- a/backend/src/seed/reset-db.js +++ b/backend/src/seed/reset-db.js @@ -1,19 +1,16 @@ import { cleanDatabase } from './factories' -import dotenv from 'dotenv' - -dotenv.config() if (process.env.NODE_ENV === 'production') { - throw new Error(`YOU CAN'T CLEAN THE DATABASE WITH NODE_ENV=${process.env.NODE_ENV}`) + throw new Error(`You cannot clean the database in production environment!`) } -(async function () { +;(async function() { try { await cleanDatabase() - console.log('Successfully deleted all nodes and relations!') + 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}`) + 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/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 8694a7948..27af1106a 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -2,126 +2,247 @@ import faker from 'faker' import Factory from './factories' /* eslint-disable no-multi-spaces */ -(async function () { +;(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' }) + 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' }) + 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' }), + 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' }) + 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', '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' }) + 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' }) + 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' }) + 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' }) + 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!' + const mention2 = + 'Hey @jenny-rostock, here is another notification for you!' await Promise.all([ - asAdmin.create('Post', { id: 'p0' }), + asAdmin.create('Post', { id: 'p0' }), asModerator.create('Post', { id: 'p1' }), - 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' }), + 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' }), asModerator.create('Post', { id: 'p7', content: `${mention1} ${faker.lorem.paragraph()}` }), - asUser.create('Post', { id: 'p8' }), - asTick.create('Post', { id: 'p9' }), - asTrick.create('Post', { id: 'p10' }), - asTrack.create('Post', { id: 'p11' }), - asAdmin.create('Post', { id: 'p12', content: `${mention2} ${faker.lorem.paragraph()}` }), + asUser.create('Post', { id: 'p8' }), + asTick.create('Post', { id: 'p9' }), + asTrick.create('Post', { id: 'p10' }), + asTrack.create('Post', { id: 'p11' }), + asAdmin.create('Post', { id: 'p12', content: `${mention2} ${faker.lorem.paragraph()}` }), asModerator.create('Post', { id: 'p13' }), - asUser.create('Post', { id: 'p14' }), - asTick.create('Post', { id: 'p15' }) + asUser.create('Post', { id: 'p14' }), + 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: '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' }), @@ -129,63 +250,45 @@ import Factory from './factories' 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: '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' }) + 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' }) + 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' }) + 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([ @@ -200,33 +303,49 @@ import Factory from './factories' 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' }) + 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' }) + 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' }) + 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.' }) + 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' }) + 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...') diff --git a/backend/src/seed/seed-helpers.js b/backend/src/seed/seed-helpers.js index 23bde40ae..399d06670 100644 --- a/backend/src/seed/seed-helpers.js +++ b/backend/src/seed/seed-helpers.js @@ -19,7 +19,7 @@ const unsplashTopics = [ 'face', 'people', 'portrait', - 'amazing' + 'amazing', ] let unsplashTopicsTmp = [] @@ -30,7 +30,7 @@ const ngoLogos = [ '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 + null, ] const difficulties = ['easy', 'medium', 'hard'] @@ -38,8 +38,7 @@ const difficulties = ['easy', 'medium', 'hard'] export default { randomItem: (items, filter) => { let ids = filter - ? Object.keys(items) - .filter(id => { + ? Object.keys(items).filter(id => { return filter(items[id]) }) : _.keys(items) @@ -61,7 +60,7 @@ export default { } return res }, - random: (items) => { + random: items => { return _.shuffle(items).pop() }, randomDifficulty: () => { @@ -78,7 +77,9 @@ export default { if (unsplashTopicsTmp.length < 2) { unsplashTopicsTmp = _.shuffle(unsplashTopics) } - return 'https://source.unsplash.com/daily?' + unsplashTopicsTmp.pop() + ',' + unsplashTopicsTmp.pop() + return ( + 'https://source.unsplash.com/daily?' + unsplashTopicsTmp.pop() + ',' + unsplashTopicsTmp.pop() + ) }, randomCategories: (seederstore, allowEmpty = false) => { let count = Math.round(Math.random() * 3) @@ -101,8 +102,8 @@ export default { zipCode: faker.address.zipCode(), street: faker.address.streetAddress(), country: faker.address.countryCode(), - lat: 54.032726 - (Math.random() * 10), - lng: 6.558838 + (Math.random() * 10) + lat: 54.032726 - Math.random() * 10, + lng: 6.558838 + Math.random() * 10, }) } return addresses @@ -129,5 +130,5 @@ export default { code += chars.substr(n, 1) } return code - } + }, } diff --git a/backend/src/server.js b/backend/src/server.js index fe0d4ee1d..7692f0d2c 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,62 +1,41 @@ -import { GraphQLServer } from 'graphql-yoga' -import { makeAugmentedSchema } from 'neo4j-graphql-js' -import { typeDefs, resolvers } from './graphql-schema' import express from 'express' -import dotenv from 'dotenv' +import helmet from 'helmet' +import { GraphQLServer } from 'graphql-yoga' +import CONFIG, { requiredConfigs } from './config' import mocks from './mocks' import middleware from './middleware' -import applyDirectives from './bootstrap/directives' -import applyScalars from './bootstrap/scalars' import { getDriver } from './bootstrap/neo4j' -import helmet from 'helmet' import decode from './jwt/decode' +import schema from './schema' -dotenv.config() -// check env and warn -const requiredEnvVars = ['MAPBOX_TOKEN', 'JWT_SECRET', 'PRIVATE_KEY_PASSPHRASE'] -requiredEnvVars.forEach(env => { - if (!process.env[env]) { - throw new Error(`ERROR: "${env}" env variable is missing.`) +// 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 debug = process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true' -let schema = makeAugmentedSchema({ - typeDefs, - resolvers, - config: { - query: { - exclude: ['Notfication', 'Statistics', 'LoggedInUser'] - }, - mutation: { - exclude: ['Notfication', 'Statistics', 'LoggedInUser'] - }, - debug: debug - } -}) -schema = applyScalars(applyDirectives(schema)) - -const createServer = (options) => { +const createServer = options => { const defaults = { context: async ({ request }) => { - const authorizationHeader = request.headers.authorization || '' - const user = await decode(driver, authorizationHeader) + const user = await decode(driver, request.headers.authorization) return { driver, user, req: request, cypherParams: { - currentUserId: user ? user.id : null - } + currentUserId: user ? user.id : null, + }, } }, - schema: schema, - debug: debug, - tracing: debug, + schema, + debug: CONFIG.DEBUG, + tracing: CONFIG.DEBUG, middlewares: middleware(schema), - mocks: (process.env.MOCK === 'true') ? mocks : false + mocks: CONFIG.MOCKS ? mocks : false, } const server = new GraphQLServer(Object.assign({}, defaults, options)) diff --git a/backend/yarn.lock b/backend/yarn.lock index 7a24e16ca..61803eb1f 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -2,17 +2,17 @@ # yarn lockfile v1 -"@apollographql/apollo-tools@^0.3.6-alpha.1": - version "0.3.6-alpha.1" - resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.3.6-alpha.1.tgz#5199b36c65c2fddc4f8bc8bb97642f74e9fb00c5" - integrity sha512-fq74In3Vw9OmtKHze0L5/Ns/pdTZOqUeFVC6Um9NRgziVehXz/qswsh2r3+dsn82uqoa/AlvckHtd6aPPPYj9g== +"@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.4.1-alpha.1" + apollo-env "0.5.1" -"@apollographql/graphql-playground-html@^1.6.6": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.6.tgz#022209e28a2b547dcde15b219f0c50f47aa5beb3" - integrity sha512-lqK94b+caNtmKFs5oUVXlSpN3sm5IXZ+KfhMxOtr0LR2SqErzkoJilitjDvJ1WbjHlxLI7WtCjRmOLdOGJqtMQ== +"@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" @@ -38,17 +38,17 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@^7.1.0", "@babel/core@~7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.4.tgz#84055750b05fcd50f9915a826b44fa347a825250" - integrity sha512-lQgGX3FPRgbz2SKmhMtYgJvVzGZrmjaF4apZ2bLwofAKiSjxU0drPh4S/VasyYXwaTs+A1gvQ45BN8SQJzHsQQ== +"@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.4" + "@babel/parser" "^7.4.5" "@babel/template" "^7.4.4" - "@babel/traverse" "^7.4.4" + "@babel/traverse" "^7.4.5" "@babel/types" "^7.4.4" convert-source-map "^1.1.0" debug "^4.1.0" @@ -278,21 +278,22 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/node@~7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.2.2.tgz#1557dd23545b38d7b1d030a9c0e8fb225dbf70ab" - integrity sha512-jPqgTycE26uFsuWpLika9Ohz9dmLQHWjOnMNxBOjYb1HXO+eLKxEr5FfKSXH/tBvFwwaw+pzke3gagnurGOfCA== +"@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.10" + 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": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.4.tgz#5977129431b8fe33471730d255ce8654ae1250b6" - integrity sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w== +"@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" @@ -524,12 +525,12 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-named-capturing-groups-regex@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.4.tgz#5611d96d987dfc4a3a81c4383bb173361037d68d" - integrity sha512-Ki+Y9nXBlKfhD+LXaRS7v95TtTGYRAf9Y1rTDiE75zf8YQz4GDaWRXosMfJBXxnk88mGFjWdCRIeqDbon7spYA== +"@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.0" + regexp-tree "^0.1.6" "@babel/plugin-transform-new-target@^7.4.4": version "7.4.4" @@ -562,12 +563,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-regenerator@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.4.tgz#5b4da4df79391895fca9e28f99e87e22cfc02072" - integrity sha512-Zz3w+pX1SI0KMIiqshFZkwnVGUhDZzpX2vtPzfJBKQQq8WsP/Xy9DNdELWivxcKOCX/Pywge4SiEaPaLtoDT4g== +"@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.13.4" + regenerator-transform "^0.14.0" "@babel/plugin-transform-reserved-words@^7.2.0": version "7.2.0" @@ -622,15 +623,7 @@ "@babel/helper-regex" "^7.4.4" regexpu-core "^4.5.4" -"@babel/polyfill@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.0.0.tgz#c8ff65c9ec3be6a1ba10113ebd40e8750fb90bff" - integrity sha512-dnrMRkyyr74CRelJwvgnnSUDh2ge2NCTyHVwpOdvRMHtJUyxLtMAfhBN3s64pY41zdw0kgiLPh6S20eb1NcX6Q== - dependencies: - core-js "^2.5.7" - regenerator-runtime "^0.11.1" - -"@babel/polyfill@^7.2.3": +"@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== @@ -638,10 +631,10 @@ core-js "^2.5.7" regenerator-runtime "^0.12.0" -"@babel/preset-env@~7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.4.tgz#b6f6825bfb27b3e1394ca3de4f926482722c1d6f" - integrity sha512-FU1H+ACWqZZqfw1x2G1tgtSSYSfxJLkpaUQL37CenULFARDo+h4xJoVHzRoHbK+85ViLciuI7ME4WTIhFRBBlw== +"@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" @@ -672,12 +665,12 @@ "@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.4" + "@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.4" + "@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" @@ -686,8 +679,8 @@ "@babel/plugin-transform-typeof-symbol" "^7.2.0" "@babel/plugin-transform-unicode-regex" "^7.4.4" "@babel/types" "^7.4.4" - browserslist "^4.5.2" - core-js-compat "^3.0.0" + browserslist "^4.6.0" + core-js-compat "^3.1.1" invariant "^2.2.2" js-levenshtein "^1.1.3" semver "^5.5.0" @@ -720,16 +713,16 @@ "@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": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.4.tgz#0776f038f6d78361860b6823887d4f3937133fe8" - integrity sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A== +"@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.4" + "@babel/parser" "^7.4.5" "@babel/types" "^7.4.4" debug "^4.1.0" globals "^11.1.0" @@ -1126,10 +1119,10 @@ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0" integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA== -"@types/yup@0.26.13": - version "0.26.13" - resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.13.tgz#0aeeba85231a34ddc68c74b3a2c64eeb2ccf68bf" - integrity sha512-sMMtb+c2xxf/FcK0kW36+0uuSWpNwvCBZYI7vpnD9J9Z6OYk09P4TmDkMWV+NWdi9Nzt2tUJjtpnPpkiUklBaw== +"@types/yup@0.26.14": + version "0.26.14" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.14.tgz#d31f3b9a04039cca70ebb4db4d6c7fc3f694e80b" + integrity sha512-OcBtVLHvYULVSltpuBdhFiVOKoSsOS58D872HydO93oBf3OdGq5zb+LnqGo18TNNSV2aW8hjIdS6H+wp68zFtQ== "@types/zen-observable@^0.5.3": version "0.5.4" @@ -1141,6 +1134,20 @@ 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" @@ -1281,13 +1288,13 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -apollo-cache-control@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.6.0.tgz#df22db28f850ea90a5722f5e92654d30c96e7f91" - integrity sha512-66aCF6MHe0/FdD3knphwTv6CCIdb1ZxrMsiRpxP474qqyYVe2jAwBu6aJBn4emffZHZ7i6gp9dY6cPHThjnbKA== +apollo-cache-control@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.2.tgz#b8852422d973c582493e85c776abc9c660090162" + integrity sha512-7prjFN8H9lRE0npqGG8kM3XICvNCcgQt6eCy8kkcPOIZwM+F8m8ShjEfNF9UWW32i+poOk3G67HegPRyjCc6/Q== dependencies: - apollo-server-env "2.3.0" - graphql-extensions "0.6.0" + apollo-server-env "2.4.0" + graphql-extensions "0.7.2" apollo-cache-control@^0.1.0: version "0.1.1" @@ -1296,73 +1303,72 @@ apollo-cache-control@^0.1.0: dependencies: graphql-extensions "^0.0.x" -apollo-cache-inmemory@~1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.5.1.tgz#265d1ee67b0bf0aca9c37629d410bfae44e62953" - integrity sha512-D3bdpPmWfaKQkWy8lfwUg+K8OBITo3sx0BHLs1B/9vIdOIZ7JNCKq3EUcAgAfInomJUdN0QG1yOfi8M8hxkN1g== +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.2.1" - apollo-utilities "^1.2.1" - optimism "^0.6.9" - ts-invariant "^0.2.1" + 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.2.1, apollo-cache@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.2.1.tgz#aae71eb4a11f1f7322adc343f84b1a39b0693644" - integrity sha512-nzFmep/oKlbzUuDyz6fS6aYhRmfpcHWqNkkA9Bbxwk18RD6LXC4eZkuE0gXRX0IibVBHNjYVK+Szi0Yied4SpQ== +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.2.1" + apollo-utilities "^1.3.2" tslib "^1.9.3" -apollo-client@~2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.5.1.tgz#36126ed1d32edd79c3713c6684546a3bea80e6d1" - integrity sha512-MNcQKiqLHdGmNJ0rZ0NXaHrToXapJgS/5kPk0FygXt+/FmDCdzqcujI7OPxEC6e9Yw5S/8dIvOXcRNuOMElHkA== +apollo-client@~2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.2.tgz#03b6af651e09b6e413e486ddc87464c85bd6e514" + integrity sha512-oks1MaT5x7gHcPeC8vPC1UzzsKaEIC0tye+jg72eMDt5OKc7BobStTeS/o2Ib3e0ii40nKxGBnMdl/Xa/p56Yg== dependencies: "@types/zen-observable" "^0.8.0" - apollo-cache "1.2.1" + apollo-cache "1.3.2" apollo-link "^1.0.0" - apollo-link-dedup "^1.0.0" - apollo-utilities "1.2.1" + apollo-utilities "1.3.2" symbol-observable "^1.0.2" - ts-invariant "^0.2.1" + ts-invariant "^0.4.0" tslib "^1.9.3" zen-observable "^0.8.0" -apollo-datasource@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.4.0.tgz#f042641fd2593fa5f4f002fc30d1fb1a20284df8" - integrity sha512-6QkgnLYwQrW0qv+yXIf617DojJbGmza2XJXUlgnzrGGhxzfAynzEjaLyYkc8rYS1m82vjrl9EOmLHTcnVkvZAQ== +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.3.0" + apollo-server-env "2.4.0" -apollo-engine-reporting-protobuf@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.3.0.tgz#2c764c054ff9968387cf16115546e0d5b04ee9f1" - integrity sha512-PYowpx/E+TJT/8nKpp3JmJuKh3x1SZcxDF6Cquj0soV205TUpFFCZQMi91i5ACiEp2AkYvM/GDBIrw+rfIwzTg== +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.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.1.0.tgz#10def3d3bf3f11ddb24765c19d9c81e30cb9d55c" - integrity sha512-Dj0BwgcluHL0QVUaquhAoYoLX9Z4DRP/n2REcIwO8d2iy52r+1wN5QqZLx97dEFh7CjhNjTWeysJzc8XMWKa1Q== +apollo-engine-reporting@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.0.tgz#50151811a0f5e70f4a73e7092a61fec422d8e722" + integrity sha512-xP+Z+wdQH4ee7xfuP3WsJcIe30AH68gpp2lQm2+rnW5JfjIqD5YehSoO2Svi2jK3CSv8Y561i3QMW9i34P7hEQ== dependencies: - apollo-engine-reporting-protobuf "0.3.0" - apollo-graphql "^0.2.1-alpha.1" - apollo-server-core "2.5.0" - apollo-server-env "2.3.0" + apollo-engine-reporting-protobuf "0.3.1" + apollo-graphql "^0.3.0" + apollo-server-core "2.6.2" + apollo-server-env "2.4.0" async-retry "^1.2.1" - graphql-extensions "0.6.0" + graphql-extensions "0.7.2" -apollo-env@0.4.1-alpha.1: - version "0.4.1-alpha.1" - resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.4.1-alpha.1.tgz#10d3ea508b8f3ba03939ef4e6ec4b2b5db77e8f1" - integrity sha512-4qWiaUKWh92jvKxxRsiZSjmW9YH9GWSG1W6X+S1BcC1uqtPiHsem7ExG9MMTt+UrzHsbpQLctj12xk8lI4lgCg== +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.0-beta.13" + core-js "^3.0.1" node-fetch "^2.2.0" sha.js "^2.4.11" @@ -1374,12 +1380,12 @@ apollo-errors@^1.9.0: assert "^1.4.1" extendable-error "^0.1.5" -apollo-graphql@^0.2.1-alpha.1: - version "0.2.1-alpha.1" - resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.2.1-alpha.1.tgz#a0cc0bd65e03c7e887c96c9f53421f3c6dd7b599" - integrity sha512-kObCSpYRHEf4IozJV+TZAXEL2Yni2DpzQckohJNYXg5/KRAF20jJ7lHxuJz+kMQrc7QO4wYGSa29HuFZH2AtQA== +apollo-graphql@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.1.tgz#d13b80cc0cae3fe7066b81b80914c6f983fac8d7" + integrity sha512-tbhtzNAAhNI34v4XY9OlZGnH7U0sX4BP1cJrUfSiNzQnZRg1UbQYZ06riHSOHpi5RSndFcA9LDM5C1ZKKOUeBg== dependencies: - apollo-env "0.4.1-alpha.1" + apollo-env "0.5.1" lodash.sortby "^4.7.0" apollo-link-context@~1.0.14: @@ -1390,13 +1396,6 @@ apollo-link-context@~1.0.14: apollo-link "^1.2.11" tslib "^1.9.3" -apollo-link-dedup@^1.0.0: - version "1.0.11" - resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.11.tgz#6f34ea748d2834850329ad03111ef18445232b05" - integrity sha512-RcvkXR0CNbQcsw6LdrPksGa+9YjZ1ghk0k2PKal6rSBCyyqzokcBawXOtoMN8q+0FLR1dGs5GnAQVeucQuY28g== - dependencies: - apollo-link "^1.2.4" - apollo-link-http-common@^0.2.13: version "0.2.13" resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.13.tgz#c688f6baaffdc7b269b2db7ae89dae7c58b5b350" @@ -1415,7 +1414,7 @@ apollo-link-http@~1.5.14: apollo-link-http-common "^0.2.13" tslib "^1.9.3" -apollo-link@^1.0.0, apollo-link@^1.2.11, apollo-link@^1.2.3, apollo-link@^1.2.4: +apollo-link@^1.0.0, apollo-link@^1.2.11, apollo-link@^1.2.3: version "1.2.11" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.11.tgz#493293b747ad3237114ccd22e9f559e5e24a194d" integrity sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA== @@ -1432,24 +1431,24 @@ apollo-server-caching@0.4.0: dependencies: lru-cache "^5.0.0" -apollo-server-core@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.5.0.tgz#89fc28ba1018ebf9240bc3cc0c103fe705309023" - integrity sha512-7hyQ/Rt0hC38bUfxMQmLNHDBIGEBykFWo9EO0W+3o/cno/SqBKd1KKichrABVv+v+SCvZAUutX6gYS5l3G+ULQ== +apollo-server-core@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.2.tgz#a792b50d4df9e26ec03759a31fbcbce38361b218" + integrity sha512-AbAnfoQ26NPsNIyBa/BVKBtA/wRsNL/E6eEem1VIhzitfgO25bVXFbEZDLxbgz6wvJ+veyRFpse7Qi1bvRpxOw== dependencies: - "@apollographql/apollo-tools" "^0.3.6-alpha.1" - "@apollographql/graphql-playground-html" "^1.6.6" + "@apollographql/apollo-tools" "^0.3.6" + "@apollographql/graphql-playground-html" "1.6.20" "@types/ws" "^6.0.0" - apollo-cache-control "0.6.0" - apollo-datasource "0.4.0" - apollo-engine-reporting "1.1.0" + apollo-cache-control "0.7.2" + apollo-datasource "0.5.0" + apollo-engine-reporting "1.3.0" apollo-server-caching "0.4.0" - apollo-server-env "2.3.0" + apollo-server-env "2.4.0" apollo-server-errors "2.3.0" - apollo-server-plugin-base "0.4.0" - apollo-tracing "0.6.0" + apollo-server-plugin-base "0.5.2" + apollo-tracing "0.7.2" fast-json-stable-stringify "^2.0.0" - graphql-extensions "0.6.0" + graphql-extensions "0.7.2" graphql-subscriptions "^1.0.0" graphql-tag "^2.9.2" graphql-tools "^4.0.0" @@ -1467,10 +1466,10 @@ apollo-server-core@^1.3.6, apollo-server-core@^1.4.0: apollo-tracing "^0.1.0" graphql-extensions "^0.0.x" -apollo-server-env@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.3.0.tgz#f0bf4484a6cc331a8c13763ded56e91beb16ba17" - integrity sha512-WIwlkCM/gir0CkoYWPMTCH8uGCCKB/aM074U1bKayvkFOBVO2VgG5x2kgsfkyF05IMQq2/GOTsKhNY7RnUEhTA== +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" @@ -1480,18 +1479,18 @@ apollo-server-errors@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.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.5.0.tgz#ff6cbd3fcb8933f6316c5a5edd4db12d9a56fa65" - integrity sha512-2gd3VWIqji2jyDYMTTqKzVU4/znjEjugtLUmPgVl5SoBvJSMTsO7VgJv+roBubZGDK8jXXUEXr2a33RtIeHe4g== +apollo-server-express@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.2.tgz#526297c01a7a32fe9215566f9fd7ff92e82f1fa0" + integrity sha512-nbL3noJ5KxKGg+hT8UsAA7++oHWq/KNSevfdCluWTfUNqH1vYRTvAnARx/6JM06S9zcPTfOLcqwHnDnY9zYFxA== dependencies: - "@apollographql/graphql-playground-html" "^1.6.6" + "@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.16.1" accepts "^1.3.5" - apollo-server-core "2.5.0" + apollo-server-core "2.6.2" body-parser "^1.18.3" cors "^2.8.4" graphql-subscriptions "^1.0.0" @@ -1519,36 +1518,36 @@ apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^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.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.4.0.tgz#38a3c37767043873dd1b07143d4e70eecbb09562" - integrity sha512-iD7ARNtwnvHGd1EMPK0CuodM8d8hgDvFwTfIDzJY04QIQ6/KrBFaWhnCXJsy+HMb47GovwBbq67IK6eb2WJgBg== +apollo-server-plugin-base@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.2.tgz#f97ba983f1e825fec49cba8ff6a23d00e1901819" + integrity sha512-j81CpadRLhxikBYHMh91X4aTxfzFnmmebEiIR9rruS6dywWCxV2aLW87l9ocD1MiueNam0ysdwZkX4F3D4csNw== -apollo-server-testing@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.5.0.tgz#6c4c386ddbcc5e555a02afc2c625955150827969" - integrity sha512-mjUjcdsm6np7dnx5Dy7v0k0cwNHIdTHuTZUUgLuYUPtJUS+QOmOQ4yNpglPnHwY8TXx/asFnKGKvrO5mUrUedA== +apollo-server-testing@~2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.2.tgz#e0ecddd565fce1c38a346f9fbe6118f543ccf6a6" + integrity sha512-I9QLFk4I/z9oOIXfnLc8RPBYAKih6Olrg3RDeRvWhDjLQ8gfALXVhCO+7WuvM35wNZcZVn7aXBeZ8Y3mlgkj8w== dependencies: - apollo-server-core "2.5.0" + apollo-server-core "2.6.2" -apollo-server@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.5.0.tgz#a88a550dbc5ff0c6713142d1cab3b61b4a36e483" - integrity sha512-85A3iAnXVP5QiXc0xvAJRyGsoxov06+8AzttKqehR4Q50UC1Is62xY5WZk58oW7fm+awpqh+sXB2F2E6tObSmg== +apollo-server@~2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.2.tgz#33fe894b740588f059a7679346516ffce50377d5" + integrity sha512-fMXaAKIb0dX0lzcZ4zlu7ay1L596d9HTNkdn8cKuM7zmTpugZSAL966COguJUDSjUS9CaB1Kh5hl1yRuRqHXSA== dependencies: - apollo-server-core "2.5.0" - apollo-server-express "2.5.0" + apollo-server-core "2.6.2" + apollo-server-express "2.6.2" express "^4.0.0" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" -apollo-tracing@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.6.0.tgz#afc2b9cbea173dc4c315a5d98053797469518083" - integrity sha512-OpYPHVBgcQ/HT2WLXJQWwhilzR1rrl01tZeMU2N7yinsp/oyKngF5aUSMtuvX1k/T3abilQo+w10oAQlBCGdPA== +apollo-tracing@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.2.tgz#7730159a4670bca465ac1bfa01f9902610a7aba4" + integrity sha512-bT4/n8Vy9DweC3+XWJelJD41FBlKMXR0OVxjLMiCe9clb4yTgKhYxRGTyh9KjmhWsng9gG/DphO0ixWsOgdXmA== dependencies: - apollo-server-env "2.3.0" - graphql-extensions "0.6.0" + apollo-server-env "2.4.0" + graphql-extensions "0.7.2" apollo-tracing@^0.1.0: version "0.1.4" @@ -1567,13 +1566,14 @@ apollo-upload-server@^7.0.0: http-errors "^1.7.0" object-path "^0.11.4" -apollo-utilities@1.2.1, apollo-utilities@^1.0.1, apollo-utilities@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.2.1.tgz#1c3a1ebf5607d7c8efe7636daaf58e7463b41b3c" - integrity sha512-Zv8Udp9XTSFiN8oyXOjf6PMHepD4yxxReLsl6dPUy5Ths7jti3nmlBzZUOxuTWRwZn0MoclqL7RQ5UEJN8MAxg== +apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.2.1, 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.2.1" + ts-invariant "^0.4.0" tslib "^1.9.3" aproba@^1.0.3: @@ -2012,14 +2012,14 @@ browser-resolve@^1.11.3: dependencies: resolve "1.1.7" -browserslist@^4.5.1, browserslist@^4.5.2: - version "4.5.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.5.4.tgz#166c4ecef3b51737a42436ea8002aeea466ea2c7" - integrity sha512-rAjx494LMjqKnMPhFkuLmLp8JWEX0o8ADTGeAbOqaF+XCvYLreZrG5uVjnPBlAQ8REZK4pzXGvp0bWgrFtKaag== +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.30000955" - electron-to-chromium "^1.3.122" - node-releases "^1.1.13" + caniuse-lite "^1.0.30000967" + electron-to-chromium "^1.3.133" + node-releases "^1.1.19" bs58@=2.0.0: version "2.0.0" @@ -2110,10 +2110,10 @@ camelize@1.0.0: resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= -caniuse-lite@^1.0.30000955: - version "1.0.30000956" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000956.tgz#fe56d8727fab96e0304ffbde6c4e538c9ad2a741" - integrity sha512-3o7L6XkQ01Oney+x2fS5UVbQXJ7QQkYxrSfaLmFlgQabcKfploI8bhS2nmQ8Unh5MpMONAMeDEdEXG9t9AK6uA== +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" @@ -2230,7 +2230,7 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" -cli-table3@^0.5.0, cli-table3@^0.5.1: +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== @@ -2394,36 +2394,35 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js-compat@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.0.0.tgz#cd9810b8000742535a4a43773866185e310bd4f7" - integrity sha512-W/Ppz34uUme3LmXWjMgFlYyGnbo1hd9JvA0LNQ4EmieqVjg2GPYbj3H6tcdP2QGPGWdRKUqZVbVKLNIFVs/HiA== +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.5.1" - core-js "3.0.0" - core-js-pure "3.0.0" - semver "^5.6.0" + browserslist "^4.6.0" + core-js-pure "3.1.2" + semver "^6.0.0" -core-js-pure@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.0.0.tgz#a5679adb4875427c8c0488afc93e6f5b7125859b" - integrity sha512-yPiS3fQd842RZDgo/TAKGgS0f3p2nxssF1H65DIZvZv0Od5CygP8puHXn3IQiM/39VAvgCbdaMQpresrbGgt9g== - -core-js@3.0.0, 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.0-beta.13: - version "3.0.0-beta.13" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0-beta.13.tgz#7732c69be5e4758887917235fe7c0352c4cb42a1" - integrity sha512-16Q43c/3LT9NyePUJKL8nRIQgYWjcBhjJSMWg96PVSxoS0PeE0NHitPI3opBrs9MGGHjte1KoEVr9W63YKlTXQ== +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" @@ -2587,10 +2586,10 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-fns@2.0.0-alpha.27: - version "2.0.0-alpha.27" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.27.tgz#5ecd4204ef0e7064264039570f6e8afbc014481c" - integrity sha512-cqfVLS+346P/Mpj2RpDrBv0P4p2zZhWWvfY5fuWrXNR/K38HaAGEkeOwb47hIpQP9Jr/TIxjZ2/sNMQwdXuGMg== +date-fns@2.0.0-alpha.31: + version "2.0.0-alpha.31" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.31.tgz#51bcfdca25dfc9bea334a556ab33dfc0bb00421c" + integrity sha512-S19PwMqnbYsqcbCg02Yj9gv4veVNZ0OX7v2+zcd+Mq0RI7LoDKJipJjnMrTZ3Cc6blDuTce5G/pHXcVIGRwJWQ== 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" @@ -2640,6 +2639,11 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deepmerge@^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" @@ -2845,10 +2849,10 @@ ee-first@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.122: - version "1.3.122" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.122.tgz#b32a0805f48557bd3c3b8104eadc7fa511b14a9a" - integrity sha512-3RKoIyCN4DhP2dsmleuFvpJAIDOseWH88wFYBzb22CSwoFDSWRc4UAMfrtc9h8nBdJjTNIN3rogChgOy6eFInw== +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" @@ -2991,6 +2995,13 @@ escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" +eslint-config-prettier@~4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-4.3.0.tgz#c55c1fcac8ce4518aeb77906984e134d9eb5a4f0" + integrity sha512-sZwhSTHVVz78+kYD3t5pCWSYEdVSBR0PXnwjDRsUs8ytIrK8PLXw+6FKp8r3Z7rx4ZszdetWlXYKOHoUrrwPlA== + 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" @@ -3020,10 +3031,10 @@ eslint-plugin-es@^1.4.0: eslint-utils "^1.3.0" regexpp "^2.0.1" -eslint-plugin-import@~2.17.2: - version "2.17.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.2.tgz#d227d5c6dc67eca71eb590d2bb62fb38d86e9fcb" - integrity sha512-m+cSVxM7oLsIpmwNn2WXTJoReOF9f/CtLMo7qOVmKd1KntBy0hEcuNZ3erTmWjx+DxRO0Zcrm5KwAvI9wHcV5g== +eslint-plugin-import@~2.17.3: + version "2.17.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.3.tgz#00548b4434c18faebaba04b24ae6198f280de189" + integrity sha512-qeVf/UwXFJbeyLbxuY8RgqDyEKCkqV7YC+E5S5uOjAp4tOc8zj01JP3ucoBM8JcEqd1qRasJSg6LLlisirfy0Q== dependencies: array-includes "^3.0.3" contains-path "^0.1.0" @@ -3035,24 +3046,31 @@ eslint-plugin-import@~2.17.2: lodash "^4.17.11" minimatch "^3.0.4" read-pkg-up "^2.0.0" - resolve "^1.10.0" + resolve "^1.11.0" -eslint-plugin-jest@~22.5.1: - version "22.5.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.5.1.tgz#a31dfe9f9513c6af7c17ece4c65535a1370f060b" - integrity sha512-c3WjZR/HBoi4GedJRwo2OGHa8Pzo1EbSVwQ2HFzJ+4t2OoYM7Alx646EH/aaxZ+9eGcPiq0FT0UGkRuFFx2FHg== +eslint-plugin-jest@~22.6.4: + version "22.6.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.6.4.tgz#2895b047dd82f90f43a58a25cf136220a21c9104" + integrity sha512-36OqnZR/uMCDxXGmTsqU4RwllR0IiB/XF8GW3ODmhsjiITKuI0GpgultWFt193ipN3HARkaIcKowpE6HBvRHNg== -eslint-plugin-node@~9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.0.1.tgz#93e44626fa62bcb6efea528cee9687663dc03b62" - integrity sha512-fljT5Uyy3lkJzuqhxrYanLSsvaILs9I7CmQ31atTtZ0DoIzRbbvInBh4cQ1CrthFHInHYBQxfPmPt6KLHXNXdw== +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.0.0" + 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.1.1: version "4.1.1" @@ -3256,10 +3274,10 @@ expect@^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.0: - version "4.17.0" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.0.tgz#288af62228a73f4c8ea2990ba3b791bb87cd4438" - integrity sha512-1Z7/t3Z5ZnBG252gKUPyItc4xdeaA0X934ca2ewckAsVsw9EG71i++ZHZPYnus8g/s5Bty8IMpSVEuRkmwwPRQ== +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" @@ -3360,6 +3378,11 @@ fast-deep-equal@^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" @@ -3571,6 +3594,11 @@ get-func-name@^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" @@ -3683,12 +3711,12 @@ graphql-deduplicator@^2.0.1: resolved "https://registry.yarnpkg.com/graphql-deduplicator/-/graphql-deduplicator-2.0.2.tgz#d8608161cf6be97725e178df0c41f6a1f9f778f3" integrity sha512-0CGmTmQh4UvJfsaTPppJAcHwHln8Ayat7yXXxdnuWT+Mb1dBzkbErabCWzjXyKh/RefqlGTTA7EQOZHofMaKJA== -graphql-extensions@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.6.0.tgz#3ee3aa57fe213f90aec5cd31275f6d04767c6a23" - integrity sha512-SshzmbD68fHXRv2q3St29olMOxHDLQ5e9TOh+Tz2BYxinrfhjFaPNcEefiK/vF295wW827Y58bdO11Xmhf8J+Q== +graphql-extensions@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.2.tgz#8711543f835661eaf24b48d6ac2aad44dbbd5506" + integrity sha512-TuVINuAOrEtzQAkAlCZMi9aP5rcZ+pVaqoBI5fD2k5O9fmb8OuXUQOW028MUhC66tg4E7h4YSF1uYUIimbu4SQ== dependencies: - "@apollographql/apollo-tools" "^0.3.6-alpha.1" + "@apollographql/apollo-tools" "^0.3.6" graphql-extensions@^0.0.x, graphql-extensions@~0.0.9: version "0.0.10" @@ -3744,12 +3772,12 @@ graphql-request@~1.8.2: dependencies: cross-fetch "2.2.2" -graphql-shield@~5.3.5: - version "5.3.5" - resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.3.5.tgz#cba409f4c1714e107212cff0a1cb2d934273392b" - integrity sha512-3kmL9x+b85NK2ipH3VGudUgUo1vXy0Z44WXhnGi3b0T0peg53DOSlXBbZOO4PNh1AcULnUjYf+DpDrP8Uc97Gw== +graphql-shield@~5.3.6: + version "5.3.6" + resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.3.6.tgz#20061b02f77056c0870a623c530ef28a1bf4fff4" + integrity sha512-ihw/i4X+d1kpj1SVA6iBkVl2DZhPsI+xV08geR2TX3FWhpU7zakk/16yBzDRJTTCUgKsWfgyebrgIBsuhTwMnA== dependencies: - "@types/yup" "0.26.13" + "@types/yup" "0.26.14" lightercollective "^0.3.0" object-hash "^1.3.1" yup "^0.27.0" @@ -3821,10 +3849,10 @@ graphql-yoga@~1.17.4: graphql-tools "^4.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.3.0: - version "14.3.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.3.0.tgz#34dd36faa489ff642bcd25df6c3b4f988a1a2f3e" - integrity sha512-MdfI4v7kSNC3NhB7cF8KNijDsifuWO2XOtzpyququqaclO8wVuChYv+KogexDwgP5sp7nFI9Z6N4QHgoLkfjrg== +"graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", graphql@^14.2.1, graphql@~14.3.1: + version "14.3.1" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.3.1.tgz#b3aa50e61a841ada3c1f9ccda101c483f8e8c807" + integrity sha512-FZm7kAa3FqKdXy8YSSpAoTtyDFMIYSpCDOr+3EqlI1bxmtHu+Vv/I2vrSeT1sBOEnEniX3uo4wFhFdS/8XN6gA== dependencies: iterall "^1.2.2" @@ -4092,11 +4120,6 @@ ignore@^5.1.1: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.1.tgz#2fc6b8f518aff48fef65a7f348ed85632448e4a5" integrity sha512-DWjnQIFLenVrwyRCKZT+7a7/U4Cqgar4WG8V++K3hw+lrW1hc/SIwdiGmtxKCVACmHULTuGeBbHJmbwW7/sAvA== -immutable-tuple@^0.4.9: - version "0.4.9" - resolved "https://registry.yarnpkg.com/immutable-tuple/-/immutable-tuple-0.4.9.tgz#473ebdd6c169c461913a454bf87ef8f601a20ff0" - integrity sha512-LWbJPZnidF8eczu7XmcnLBsumuyRBkpwIRPCZxlojouhBo5jEBO4toj6n7hMy6IxHU/c+MqDSWkvaTpPlMQcyA== - import-fresh@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.0.0.tgz#a3d897f420cab0e671236897f75bc14b4885c390" @@ -5353,6 +5376,15 @@ merge-descriptors@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" @@ -5490,7 +5522,7 @@ ms@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, ms@~2.1.1: +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== @@ -5564,10 +5596,9 @@ neo4j-driver@^1.7.3, neo4j-driver@~1.7.4: text-encoding "^0.6.4" uri-js "^4.2.1" -neo4j-graphql-js@~2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.6.0.tgz#1c418c5e4de384bd0992f99538d3d056d0602669" - integrity sha512-YStuqeBg6sjXQvQjICz+jpG4W58yHz3Fi1Gjw9HsVYeCSr97ARbGn4Sl02970IKpBeRYqKt1N1Xp3QIvO3OMIw== +"neo4j-graphql-js@git+https://github.com/Human-Connection/neo4j-graphql-js.git#temporary_fixes": + version "2.6.1" + resolved "git+https://github.com/Human-Connection/neo4j-graphql-js.git#84d529b9ecbc5c284cce4f86238c6d19b192cf0f" dependencies: graphql "^14.2.1" graphql-auth-directives "^2.1.0" @@ -5596,6 +5627,14 @@ nocache@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" @@ -5647,17 +5686,17 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -node-releases@^1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.13.tgz#8c03296b5ae60c08e2ff4f8f22ae45bd2f210083" - integrity sha512-fKZGviSXR6YvVPyc011NHuJDSD8gFQvLPmc2d2V3BS4gr52ycyQ1Xzs7a8B+Ax3Ni/W+5h1h4SqmzeoA8WZRmA== +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" -nodemon@~1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.0.tgz#358e005549a1e9e1148cb2b9b8b28957dc4e4527" - integrity sha512-NHKpb/Je0Urmwi3QPDHlYuFY9m1vaVfTsRZG5X73rY46xPj0JpNe8WhUGQdkDXQDOxrBNIU3JrcflE9Y44EcuA== +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" @@ -5851,12 +5890,12 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" -optimism@^0.6.9: - version "0.6.9" - resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.6.9.tgz#19258ff8b3be0cea29ac35f06bff818e026e30bb" - integrity sha512-xoQm2lvXbCA9Kd7SCx6y713Y7sZ6fUc5R6VYpoL5M6svKJbTuvtNopexK8sO8K4s0EOUYHuPN2+yAEsNyRggkQ== +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: - immutable-tuple "^0.4.9" + "@wry/context" "^0.4.0" optimist@^0.6.1: version "0.6.1" @@ -6184,6 +6223,18 @@ prepend-http@^1.0.1: 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.17.1: + version "1.17.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.1.tgz#ed64b4e93e370cb8a25b9ef7fef3e4fd1c0995db" + integrity sha512-TzGRNvuUSmPgwivDqkZ9tM/qTGW9hqDKWOE9YHiyQdixlKbv7kvEqsmDPrcHJTKwthU774TQwZXVtaQ/mMsvjg== + pretty-format@^24.8.0: version "24.8.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2" @@ -6476,7 +6527,7 @@ regenerate@^1.4.0: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== -regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1: +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== @@ -6491,10 +6542,10 @@ regenerator-runtime@^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.13.4: - version "0.13.4" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb" - integrity sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A== +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" @@ -6506,14 +6557,10 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp-tree@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.1.tgz#27b455f9b138ca2e84c090e9aff1ffe2a04d97fa" - integrity sha512-HwRjOquc9QOwKTgbxvZTcddS5mlNlwePMQ3NFL8broajMLD5CXDAqas8Y5yxJH5QtZp5iRor3YCILd5pz71Cgw== - dependencies: - cli-table3 "^0.5.0" - colors "^1.1.2" - yargs "^12.0.5" +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" @@ -6653,10 +6700,10 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.10.0, resolve@^1.10.1, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.1.tgz#664842ac960795bbe758221cdccda61fb64b5f18" - integrity sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA== +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" @@ -6797,15 +6844,15 @@ semver-diff@^2.0.0: 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: - version "5.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" - integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== +"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: - version "6.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" - integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ== +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" @@ -7496,13 +7543,6 @@ trunc-text@1.0.1: resolved "https://registry.yarnpkg.com/trunc-text/-/trunc-text-1.0.1.tgz#58f876d8ac59b224b79834bb478b8656e69622b5" integrity sha1-WPh22KxZsiS3mDS7R4uGVuaWIrU= -ts-invariant@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.2.1.tgz#3d587f9d6e3bded97bf9ec17951dd9814d5a9d3f" - integrity sha512-Z/JSxzVmhTo50I+LKagEISFJW3pvPCqsMWLamCTX8Kr3N5aMrnGOqcflbe5hLUzwjvgPfnLzQtHZv0yWQ+FIHg== - dependencies: - tslib "^1.9.3" - ts-invariant@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.3.2.tgz#89a2ffeb70879b777258df1df1c59383c35209b0" @@ -7510,6 +7550,13 @@ ts-invariant@^0.3.2: dependencies: tslib "^1.9.3" +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" @@ -8035,7 +8082,7 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@^12.0.2, yargs@^12.0.5: +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== diff --git a/cypress/fixtures/onourjourney.png b/cypress/fixtures/onourjourney.png new file mode 100644 index 000000000..8e606fabd Binary files /dev/null and b/cypress/fixtures/onourjourney.png differ diff --git a/cypress/integration/common/post.js b/cypress/integration/common/post.js index f6a1bbedd..814159a34 100644 --- a/cypress/integration/common/post.js +++ b/cypress/integration/common/post.js @@ -1,25 +1,28 @@ -import { When, Then } from 'cypress-cucumber-preprocessor/steps' +import { When, Then } from "cypress-cucumber-preprocessor/steps"; -const narratorAvatar = 'https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg' +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("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("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) -}) +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') -}) +Then("the editor should be cleared", () => { + cy.get(".ProseMirror p").should("have.class", "is-empty"); +}); diff --git a/cypress/integration/common/profile.js b/cypress/integration/common/profile.js new file mode 100644 index 000000000..1df1e2652 --- /dev/null +++ b/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/cypress/integration/common/settings.js b/cypress/integration/common/settings.js index b6621ec87..664ffcff8 100644 --- a/cypress/integration/common/settings.js +++ b/cypress/integration/common/settings.js @@ -45,6 +45,7 @@ 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) }) diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 387f33ac0..8f5bcc8ea 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -230,7 +230,7 @@ When('I type in the following text:', text => { Then('the post shows up on the landing page at position {int}', index => { cy.openPage('landing') - const selector = `:nth-child(${index}) > .ds-card > .ds-card-content` + const selector = `.post-card:nth-child(${index}) > .ds-card-content` cy.get(selector).should('contain', lastPost.title) cy.get(selector).should('contain', lastPost.content) }) diff --git a/cypress/integration/user_profile/UploadUserProfileImage.feature b/cypress/integration/user_profile/UploadUserProfileImage.feature new file mode 100644 index 000000000..b46a31de8 --- /dev/null +++ b/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/cypress/support/commands.js b/cypress/support/commands.js index a7cb76a27..f6253af20 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -13,7 +13,7 @@ // Cypress.Commands.add('login', (email, password) => { ... }) /* globals Cypress cy */ - +import 'cypress-file-upload' import { getLangByName } from './helpers' import users from '../fixtures/users.json' diff --git a/deployment/digital-ocean/https/README.md b/deployment/digital-ocean/https/README.md index c30ac958f..d100ba8dd 100644 --- a/deployment/digital-ocean/https/README.md +++ b/deployment/digital-ocean/https/README.md @@ -32,10 +32,10 @@ Once you are done, apply the configuration: $ kubectl apply -f . ``` -By now, your cluster should have an external IP address assigned. If you visit -your dashboard, this is how it should look like: +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: diff --git a/deployment/digital-ocean/https/ip-address.png b/deployment/digital-ocean/https/ip-address.png index 765177401..db523156a 100644 Binary files a/deployment/digital-ocean/https/ip-address.png and b/deployment/digital-ocean/https/ip-address.png differ diff --git a/deployment/human-connection/README.md b/deployment/human-connection/README.md index 887b2300c..8b30e98d6 100644 --- a/deployment/human-connection/README.md +++ b/deployment/human-connection/README.md @@ -6,7 +6,7 @@ just apply our provided configuration files to your cluster. ## Configuration -Copy our provided templates: +Change into the `./deployment` directory and copy our provided templates: ```bash # in folder deployment/human-connection/ @@ -14,7 +14,7 @@ $ cp templates/secrets.template.yaml ./secrets.yaml $ cp templates/configmap.template.yaml ./configmap.yaml ``` -Change the `configmap.yaml` as needed, all variables will be available as +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: @@ -28,7 +28,7 @@ If you want to edit secrets, you have to `base64` encode them. See [kubernetes d ```bash # example how to base64 a string: -$ echo -n 'admin' | base64 --wrap 0 +$ echo -n 'admin' | base64 YWRtaW4= ``` @@ -38,7 +38,7 @@ your deployed kubernetes pods. ## Create a namespace ```bash -# in folder deployment/human-connection/ +# in folder deployment/ $ kubectl apply -f namespace.yaml ``` diff --git a/deployment/human-connection/templates/configmap.template.yaml b/deployment/human-connection/templates/configmap.template.yaml index baf41661a..87b51a7d3 100644 --- a/deployment/human-connection/templates/configmap.template.yaml +++ b/deployment/human-connection/templates/configmap.template.yaml @@ -4,7 +4,7 @@ data: GRAPHQL_PORT: "4000" GRAPHQL_URI: "http://nitro-backend.human-connection:4000" - MOCK: "false" + MOCKS: "false" NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687" NEO4J_USER: "neo4j" NEO4J_AUTH: "none" diff --git a/deployment/legacy-migration/README.md b/deployment/legacy-migration/README.md index 8cc7bd746..7e8b6a205 100644 --- a/deployment/legacy-migration/README.md +++ b/deployment/legacy-migration/README.md @@ -56,7 +56,7 @@ Deploy one-time maintenance-worker pod: ```bash # in deployment/legacy-migration/ -$ kubectl apply -f db-migration-worker.yaml +$ kubectl apply -f maintenance-worker.yaml pod/nitro-maintenance-worker created ``` @@ -65,7 +65,7 @@ Import legacy database and uploads: ```bash $ kubectl --namespace=human-connection exec -it nitro-maintenance-worker bash $ import_legacy_db -$ import_uploads +$ import_legacy_uploads $ exit ``` diff --git a/deployment/legacy-migration/maintenance-worker.yaml b/deployment/legacy-migration/maintenance-worker.yaml index cda17400a..a0f354fc9 100644 --- a/deployment/legacy-migration/maintenance-worker.yaml +++ b/deployment/legacy-migration/maintenance-worker.yaml @@ -8,6 +8,9 @@ containers: - name: nitro-maintenance-worker image: humanconnection/maintenance-worker:latest + env: + - name: NEO4J_apoc_import_file_enabled + value: "true" envFrom: - configMapRef: name: maintenance-worker @@ -18,7 +21,7 @@ readOnly: false mountPath: /root/.ssh - name: uploads - mountPath: /nitro-backend/public/uploads + mountPath: /uploads - name: neo4j-data mountPath: /data/ volumes: diff --git a/deployment/legacy-migration/maintenance-worker/Dockerfile b/deployment/legacy-migration/maintenance-worker/Dockerfile index 1fafce5e8..4502d8d69 100644 --- a/deployment/legacy-migration/maintenance-worker/Dockerfile +++ b/deployment/legacy-migration/maintenance-worker/Dockerfile @@ -3,9 +3,19 @@ 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 migration /migration COPY ./binaries/* /usr/local/bin/ diff --git a/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_db b/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_db index 233798527..6ffdf8e3f 100755 --- a/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_db +++ b/deployment/legacy-migration/maintenance-worker/binaries/import_legacy_db @@ -8,5 +8,5 @@ do fi done -/migration/mongo/import.sh +/migration/mongo/export.sh /migration/neo4j/import.sh diff --git a/deployment/legacy-migration/maintenance-worker/migration/mongo/.env b/deployment/legacy-migration/maintenance-worker/migration/mongo/.env new file mode 100644 index 000000000..4c5f9e18c --- /dev/null +++ b/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/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh b/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh new file mode 100755 index 000000000..abed9b0f5 --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh @@ -0,0 +1,45 @@ +#!/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 --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/ +} + +# 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 "contributions" +export_collection "emotions" +export_collection "follows" +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/deployment/legacy-migration/maintenance-worker/migration/mongo/import.sh b/deployment/legacy-migration/maintenance-worker/migration/mongo/import.sh deleted file mode 100755 index d68a8c2a8..000000000 --- a/deployment/legacy-migration/maintenance-worker/migration/mongo/import.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -e - -echo "SSH_USERNAME ${SSH_USERNAME}" -echo "SSH_HOST ${SSH_HOST}" -echo "MONGODB_USERNAME ${MONGODB_USERNAME}" -echo "MONGODB_PASSWORD ${MONGODB_PASSWORD}" -echo "MONGODB_DATABASE ${MONGODB_DATABASE}" -echo "MONGODB_AUTH_DB ${MONGODB_AUTH_DB}" -echo "-------------------------------------------------" - - -rm -rf /tmp/mongo-export/* -mkdir -p /tmp/mongo-export/ - -ssh -4 -M -S my-ctrl-socket -fnNT -L 27018:localhost:27017 -l ${SSH_USERNAME} ${SSH_HOST} - -for collection in "categories" "badges" "users" "contributions" "comments" "follows" "shouts" -do - mongoexport --db ${MONGODB_DATABASE} --host localhost -d ${MONGODB_DATABASE} --port 27018 --username ${MONGODB_USERNAME} --password ${MONGODB_PASSWORD} --authenticationDatabase ${MONGODB_AUTH_DB} --collection $collection --collection $collection --out "/tmp/mongo-export/$collection.json" - mkdir -p /tmp/mongo-export/splits/$collection/ - split -l 1000 -a 3 /tmp/mongo-export/$collection.json /tmp/mongo-export/splits/$collection/ -done - -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/deployment/legacy-migration/maintenance-worker/migration/neo4j/.env b/deployment/legacy-migration/maintenance-worker/migration/neo4j/.env new file mode 100644 index 000000000..16220f3e6 --- /dev/null +++ b/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/_delete_all.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/_delete_all.cql new file mode 100644 index 000000000..d01871300 --- /dev/null +++ b/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql index 6b6a09592..62cd4a2cc 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql @@ -1,4 +1,46 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as badge +/* +// 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, diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges_delete.cql new file mode 100644 index 000000000..2a6f8c244 --- /dev/null +++ b/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories.cql index 776811bec..0862fe0d9 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories.cql @@ -1,4 +1,35 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as category +/* +// 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, @@ -8,6 +39,7 @@ 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' diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/categories_delete.cql new file mode 100644 index 000000000..c06b5ef2b --- /dev/null +++ b/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments.cql index 234d29d26..1cdc1bfc2 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments.cql @@ -1,15 +1,65 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as json +/* +// 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 } + } +*/ -MERGE (comment:Comment {id: json._id["$oid"]}) +CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as comment +MERGE (c:Comment {id: comment._id["$oid"]}) ON CREATE SET -comment.content = json.content, -comment.contentExcerpt = json.contentExcerpt, -comment.deleted = json.deleted, -comment.disabled = false -WITH comment, json, json.contributionId as postId +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 comment, post, json.userId as userId +WITH c, post, comment.userId as userId MATCH (author:User {id: userId}) -MERGE (comment)-[:COMMENTS]->(post) -MERGE (author)-[:WROTE]->(comment) +MERGE (c)-[:COMMENTS]->(post) +MERGE (author)-[:WROTE]->(c) ; diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/comments_delete.cql new file mode 100644 index 000000000..c4a7961c5 --- /dev/null +++ b/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql index 01647f7fb..98d8f24e9 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql @@ -1,4 +1,132 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as post +/* +// 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: { +[ ] 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 }, +[?] teaserImg: { type: String }, // Path is incorrect in Nitro +[ ] 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: { +[X] 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, @@ -20,6 +148,6 @@ MATCH (c:Category {id: categoryId}) MERGE (p)-[:CATEGORIZED]->(c) WITH p, post.tags AS tags UNWIND tags AS tag -MERGE (t:Tag {id: apoc.create.uuid(), name: tag}) +MERGE (t:Tag {id: tag, name: tag}) MERGE (p)-[:TAGGED]->(t) ; diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions_delete.cql new file mode 100644 index 000000000..70adad664 --- /dev/null +++ b/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions.cql new file mode 100644 index 000000000..8aad9e923 --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions_delete.cql new file mode 100644 index 000000000..e69de29bb diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows.cql index 0cd6d9cfc..fac858a9a 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows.cql @@ -1,4 +1,36 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as follow +/* +// 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/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows_delete.cql new file mode 100644 index 000000000..3624448c3 --- /dev/null +++ b/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh index b7de74782..ac256e3f0 100755 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh @@ -1,17 +1,100 @@ #!/usr/bin/env bash set -e -SECONDS=0 -SCRIPT_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# import .env config +set -o allexport +source $(dirname "$0")/.env +set +o allexport -echo "MATCH (n) DETACH DELETE n;" | cypher-shell +# Delete collection function defintion +function delete_collection () { + # Delete from Database + echo "Delete $1" + "${IMPORT_CYPHERSHELL_BIN}" < $(dirname "$0")/$1_delete.cql > /dev/null + # Delete index file + rm -f "${IMPORT_PATH}splits/$1.index" +} -for collection in "badges" "categories" "users" "follows" "contributions" "shouts" "comments" -do - for chunk in /tmp/mongo-export/splits/$collection/* +# 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 - mv $chunk /tmp/mongo-export/splits/current-chunk.json - echo "Import ${chunk}" && cypher-shell < $SCRIPT_DIRECTORY/$collection.cql + 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")/$1.cql)" + # 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 -done +} + +# Time variable +SECONDS=0 + +# Delete all Neo4J Database content +echo "Deleting Database Contents" +delete_collection "badges" +delete_collection "categories" +delete_collection "users" +delete_collection "follows" +delete_collection "contributions" +delete_collection "shouts" +delete_collection "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" +import_collection "categories" +import_collection "users" +import_collection "follows" +import_collection "contributions" +import_collection "shouts" +import_collection "comments" + +# import_collection "emotions" +# import_collection "invites" +# import_collection "notifications" +# import_collection "organizations" +# import_collection "pages" +# import_collection "projects" +# import_collection "settings" +# import_collection "status" +# import_collection "systemnotifications" +# import_collection "userscandos" +# import_collection "usersettings" + +echo "DONE" + echo "Time elapsed: $SECONDS seconds" diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites.cql new file mode 100644 index 000000000..f4a5bf006 --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/invites_delete.cql new file mode 100644 index 000000000..e69de29bb diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications.cql new file mode 100644 index 000000000..aa6ac8eb9 --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/notifications_delete.cql new file mode 100644 index 000000000..e69de29bb diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations.cql new file mode 100644 index 000000000..e473e697c --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/organizations_delete.cql new file mode 100644 index 000000000..e69de29bb diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages.cql new file mode 100644 index 000000000..18223136b --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/pages_delete.cql new file mode 100644 index 000000000..e69de29bb diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects.cql new file mode 100644 index 000000000..ed859c157 --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/projects_delete.cql new file mode 100644 index 000000000..e69de29bb diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings.cql new file mode 100644 index 000000000..1d557d30c --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/settings_delete.cql new file mode 100644 index 000000000..e69de29bb diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts.cql index 5019cdc32..d370b4b4a 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts.cql @@ -1,4 +1,36 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as shout +/* +// 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/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/shouts_delete.cql new file mode 100644 index 000000000..21c2e1f90 --- /dev/null +++ b/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/status.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/status.cql new file mode 100644 index 000000000..010c2ca09 --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/status_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/status_delete.cql new file mode 100644 index 000000000..e69de29bb diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications.cql new file mode 100644 index 000000000..4bd33eb7c --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/systemnotifications_delete.cql new file mode 100644 index 000000000..e69de29bb diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql index c877f8377..aec5499fc 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql @@ -1,4 +1,101 @@ -CALL apoc.load.json('file:/tmp/mongo-export/splits/current-chunk.json') YIELD value as user +/* +// 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 }, +[?] avatar: { type: String }, // Path is incorrect in Nitro +[?] coverImg: { type: String }, // Path is incorrect in Nitro, was not modeled in latest Nitro - do we want this? +[ ] 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, @@ -8,6 +105,7 @@ u.password = user.password, u.avatar = user.avatar, u.coverImg = user.coverImg, u.wasInvited = user.wasInvited, +u.wasSeeded = user.wasSeeded, u.role = toLower(user.role), u.createdAt = user.createdAt.`$date`, u.updatedAt = user.updatedAt.`$date`, diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users_delete.cql new file mode 100644 index 000000000..23935b3e0 --- /dev/null +++ b/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos.cql new file mode 100644 index 000000000..55f58f171 --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/userscandos_delete.cql new file mode 100644 index 000000000..e69de29bb diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings.cql new file mode 100644 index 000000000..722625944 --- /dev/null +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/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/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings_delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/usersettings_delete.cql new file mode 100644 index 000000000..e69de29bb diff --git a/deployment/human-connection/namespace.yaml b/deployment/namespace.yaml similarity index 100% rename from deployment/human-connection/namespace.yaml rename to deployment/namespace.yaml diff --git a/deployment/volumes/uploads.yaml b/deployment/volumes/uploads.yaml index 11a8027e9..2bd64c9ee 100644 --- a/deployment/volumes/uploads.yaml +++ b/deployment/volumes/uploads.yaml @@ -9,4 +9,4 @@ - ReadWriteOnce resources: requests: - storage: 2Gi + storage: 25Gi diff --git a/docker-compose.maintenance.yml b/docker-compose.maintenance.yml index 113b4492c..e536b1157 100644 --- a/docker-compose.maintenance.yml +++ b/docker-compose.maintenance.yml @@ -19,7 +19,7 @@ services: - GRAPHQL_URI=http://localhost:4000 - CLIENT_URI=http://localhost:3000 - JWT_SECRET=b/&&7b78BF&fv/Vd - - MOCK=false + - MOCKS=false - MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ - PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78 - NEO4J_apoc_import_file_enabled=true @@ -30,6 +30,7 @@ services: - "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 diff --git a/docker-compose.yml b/docker-compose.yml index 896d1bef9..ca66217c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ services: - GRAPHQL_URI=http://localhost:4000 - CLIENT_URI=http://localhost:3000 - JWT_SECRET=b/&&7b78BF&fv/Vd - - MOCK=false + - MOCKS=false - MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ - PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78 neo4j: diff --git a/neo4j/.env.template b/neo4j/.env.template new file mode 100644 index 000000000..c58edee0e --- /dev/null +++ b/neo4j/.env.template @@ -0,0 +1,2 @@ +NEO4J_USERNAME=neo4j +NEO4J_PASSWORD=letmein diff --git a/neo4j/.gitignore b/neo4j/.gitignore new file mode 100644 index 000000000..4c49bd78f --- /dev/null +++ b/neo4j/.gitignore @@ -0,0 +1 @@ +.env diff --git a/neo4j/Dockerfile b/neo4j/Dockerfile index e94a89431..2c106882f 100644 --- a/neo4j/Dockerfile +++ b/neo4j/Dockerfile @@ -1,3 +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/ -COPY migrate.sh /usr/local/bin/migrate +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/neo4j/README.md b/neo4j/README.md new file mode 100644 index 000000000..379a89eec --- /dev/null +++ b/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/neo4j/db_setup.sh b/neo4j/db_setup.sh new file mode 100755 index 000000000..21ed54571 --- /dev/null +++ b/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/neo4j/entrypoint.sh b/neo4j/entrypoint.sh new file mode 100755 index 000000000..f9c1afbe1 --- /dev/null +++ b/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/neo4j/migrate.sh b/neo4j/migrate.sh deleted file mode 100755 index 6f3361b8a..000000000 --- a/neo4j/migrate.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -# If the user has the password `neo4j` this is a strong indicator, that we are -# the initial default user. Before we can create constraints, we have to change -# the default password. This is a security feature of neo4j. -if echo ":exit" | cypher-shell --password neo4j 2> /dev/null ; then - if [[ -z "${NEO4J_PASSWORD}" ]]; then - echo "NEO4J_PASSWORD environment variable is undefined. I cannot set the initial password." - else - echo "CALL dbms.security.changePassword('${NEO4J_PASSWORD}');" | cypher-shell --password neo4j - fi -fi - -set -e - -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 "Successfully created all indices and unique constraints:" -echo 'CALL db.indexes();' | cypher-shell diff --git a/package.json b/package.json index 29950b65c..dd7454c54 100644 --- a/package.json +++ b/package.json @@ -21,13 +21,14 @@ "devDependencies": { "codecov": "^3.5.0", "cross-env": "^5.2.0", - "cypress": "^3.2.0", - "cypress-cucumber-preprocessor": "^1.11.0", - "cypress-plugin-retries": "^1.2.1", + "cypress": "^3.3.1", + "cypress-cucumber-preprocessor": "^1.11.2", + "cypress-file-upload": "^3.1.2", + "cypress-plugin-retries": "^1.2.2", "dotenv": "^8.0.0", "faker": "^4.1.0", "graphql-request": "^1.8.2", - "neo4j-driver": "^1.7.4", + "neo4j-driver": "^1.7.5", "npm-run-all": "^4.1.5" } -} +} \ No newline at end of file diff --git a/scripts/docker_push.sh b/scripts/docker_push.sh index 1f627cf1a..c70367005 100755 --- a/scripts/docker_push.sh +++ b/scripts/docker_push.sh @@ -2,5 +2,9 @@ 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/webapp/.eslintrc.js b/webapp/.eslintrc.js index fdc9bfb5e..f5aa82c81 100644 --- a/webapp/.eslintrc.js +++ b/webapp/.eslintrc.js @@ -2,24 +2,32 @@ module.exports = { root: true, env: { browser: true, - node: true + node: true, + jest: true }, parserOptions: { parser: 'babel-eslint' }, extends: [ - 'plugin:vue/recommended', + 'standard', + 'plugin:vue/essential', 'plugin:prettier/recommended' ], // required to lint *.vue files plugins: [ 'vue', - 'prettier' + 'prettier', + 'jest' ], // add your custom rules here rules: { - 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', + //'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'] + 'vue/component-name-in-template-casing': ['error', 'kebab-case'], + 'prettier/prettier': ['error', { + htmlWhitespaceSensitivity: 'ignore' + }], + // 'newline-per-chained-call': [2] } } diff --git a/webapp/.prettierrc b/webapp/.prettierrc deleted file mode 100644 index 7dc4f8263..000000000 --- a/webapp/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "semi": false, - "singleQuote": true, - "tabWidth": 2, - "bracketSpacing": true -} diff --git a/webapp/.prettierrc.js b/webapp/.prettierrc.js new file mode 100644 index 000000000..e2cf91e91 --- /dev/null +++ b/webapp/.prettierrc.js @@ -0,0 +1,9 @@ + +module.exports = { + semi: false, + printWidth: 100, + singleQuote: true, + trailingComma: "all", + tabWidth: 2, + bracketSpacing: true +}; diff --git a/webapp/Dockerfile b/webapp/Dockerfile index 7274693a4..feba44c36 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -1,4 +1,4 @@ -FROM node:10-alpine as base +FROM node:12.4-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 @@ -18,7 +18,7 @@ COPY . . FROM base as build-and-test RUN cp .env.template .env -RUN yarn install --production=false --frozen-lockfile --non-interactive +RUN yarn install --ignore-engines --production=false --frozen-lockfile --non-interactive RUN yarn run build FROM base as production diff --git a/webapp/components/Avatar/Avatar.spec.js b/webapp/components/Avatar/Avatar.spec.js new file mode 100644 index 000000000..ae91fecfe --- /dev/null +++ b/webapp/components/Avatar/Avatar.spec.js @@ -0,0 +1,69 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import Styleguide from '@human-connection/styleguide' +import Avatar from './Avatar.vue' + +const localVue = createLocalVue() +localVue.use(Styleguide) + +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: 'http://lorempixel.com/640/480/animals', + }, + } + }) + + it('keeps the avatar URL as is', () => { + // e.g. our seeds have absolute image URLs + expect( + Wrapper() + .find('img') + .attributes('src'), + ).toBe('http://lorempixel.com/640/480/animals') + }) + }) + }) +}) diff --git a/webapp/components/Avatar/Avatar.vue b/webapp/components/Avatar/Avatar.vue new file mode 100644 index 000000000..0d997c745 --- /dev/null +++ b/webapp/components/Avatar/Avatar.vue @@ -0,0 +1,28 @@ + +