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. ![GraphQL Playground](../.gitbook/assets/graphql-playground.png) -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} */ -async function asyncForEach (array, callback) { +async function asyncForEach(array, callback) { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array) } diff --git a/backend/src/helpers/walkRecursive.js b/backend/src/helpers/walkRecursive.js index 2f60de2a4..db9a4c703 100644 --- a/backend/src/helpers/walkRecursive.js +++ b/backend/src/helpers/walkRecursive.js @@ -1,10 +1,10 @@ /** - * iterate through all fields and replace it with the callback result - * @property data Array - * @property fields Array - * @property callback Function - */ -function walkRecursive (data, fields, callback, _key) { + * iterate through all fields and replace it with the callback result + * @property data Array + * @property fields Array + * @property callback Function + */ +function walkRecursive(data, fields, callback, _key) { if (!Array.isArray(fields)) { throw new Error('please provide an fields array for the walkRecursive helper') } diff --git a/backend/src/index.js b/backend/src/index.js index 843639aa8..f28e58947 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,17 +1,18 @@ import createServer from './server' import ActivityPub from './activitypub/ActivityPub' +import CONFIG from './config' const serverConfig = { - port: process.env.GRAPHQL_PORT || 4000 + port: CONFIG.GRAPHQL_PORT, // cors: { // credentials: true, - // origin: [process.env.CLIENT_URI] // your frontend url. + // origin: [CONFIG.CLIENT_URI] // your frontend url. // } } const server = createServer() server.start(serverConfig, options => { /* eslint-disable-next-line no-console */ - console.log(`GraphQLServer ready at ${process.env.GRAPHQL_URI} 🚀`) + console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`) ActivityPub.init(server) }) diff --git a/backend/src/jest/helpers.js b/backend/src/jest/helpers.js index 0d358ed40..d07bc9ad1 100644 --- a/backend/src/jest/helpers.js +++ b/backend/src/jest/helpers.js @@ -4,13 +4,13 @@ import { request } from 'graphql-request' // not to be confused with the seeder host export const host = 'http://127.0.0.1:4123' -export async function login ({ email, password }) { +export async function login({ email, password }) { const mutation = ` mutation { login(email:"${email}", password:"${password}") }` const response = await request(host, mutation) return { - authorization: `Bearer ${response.login}` + authorization: `Bearer ${response.login}`, } } diff --git a/backend/src/jwt/decode.js b/backend/src/jwt/decode.js index e8305a83b..b98357103 100644 --- a/backend/src/jwt/decode.js +++ b/backend/src/jwt/decode.js @@ -1,13 +1,14 @@ import jwt from 'jsonwebtoken' +import CONFIG from './../config' export default async (driver, authorizationHeader) => { if (!authorizationHeader) return null const token = authorizationHeader.replace('Bearer ', '') let id = null try { - const decoded = await jwt.verify(token, process.env.JWT_SECRET) + const decoded = await jwt.verify(token, CONFIG.JWT_SECRET) id = decoded.sub - } catch { + } catch (err) { return null } const session = driver.session() @@ -18,13 +19,13 @@ export default async (driver, authorizationHeader) => { ` const result = await session.run(query, { id }) session.close() - const [currentUser] = await result.records.map((record) => { + const [currentUser] = await result.records.map(record => { return record.get('user') }) if (!currentUser) return null if (currentUser.disabled) return null return { token, - ...currentUser + ...currentUser, } } diff --git a/backend/src/jwt/encode.js b/backend/src/jwt/encode.js index f32fc12da..1552804cc 100644 --- a/backend/src/jwt/encode.js +++ b/backend/src/jwt/encode.js @@ -1,16 +1,15 @@ - import jwt from 'jsonwebtoken' -import ms from 'ms' +import CONFIG from './../config' // Generate an Access Token for the given User ID -export default function encode (user) { - const token = jwt.sign(user, process.env.JWT_SECRET, { - expiresIn: ms('1d'), - issuer: process.env.GRAPHQL_URI, - audience: process.env.CLIENT_URI, - subject: user.id.toString() +export default function encode(user) { + const token = jwt.sign(user, CONFIG.JWT_SECRET, { + expiresIn: 24 * 60 * 60 * 1000, // one day + issuer: CONFIG.GRAPHQL_URI, + audience: CONFIG.CLIENT_URI, + subject: user.id.toString(), }) - // jwt.verifySignature(token, process.env.JWT_SECRET, (err, data) => { + // jwt.verifySignature(token, CONFIG.JWT_SECRET, (err, data) => { // console.log('token verification:', err, data) // }) return token diff --git a/backend/src/middleware/activityPubMiddleware.js b/backend/src/middleware/activityPubMiddleware.js index dcb5ae93c..f3ced42f9 100644 --- a/backend/src/middleware/activityPubMiddleware.js +++ b/backend/src/middleware/activityPubMiddleware.js @@ -1,10 +1,8 @@ import { generateRsaKeyPair } from '../activitypub/security' import { activityPub } from '../activitypub/ActivityPub' import as from 'activitystrea.ms' -import dotenv from 'dotenv' const debug = require('debug')('backend:schema') -dotenv.config() export default { Mutation: { @@ -22,13 +20,15 @@ export default { .id(`${actorId}/status/${args.activityId}`) .actor(`${actorId}`) .object( - as.article() + as + .article() .id(`${actorId}/status/${post.id}`) .content(post.content) .to('https://www.w3.org/ns/activitystreams#Public') .publishedNow() - .attributedTo(`${actorId}`) - ).prettyWrite((err, doc) => { + .attributedTo(`${actorId}`), + ) + .prettyWrite((err, doc) => { if (err) { reject(err) } else { @@ -51,6 +51,6 @@ export default { Object.assign(args, keys) args.actorId = `${activityPub.host}/activitypub/users/${args.slug}` return resolve(root, args, context, info) - } - } + }, + }, } diff --git a/backend/src/middleware/dateTimeMiddleware.js b/backend/src/middleware/dateTimeMiddleware.js index 11b6498a4..ac6e0ac4a 100644 --- a/backend/src/middleware/dateTimeMiddleware.js +++ b/backend/src/middleware/dateTimeMiddleware.js @@ -1,9 +1,9 @@ const setCreatedAt = (resolve, root, args, context, info) => { - args.createdAt = (new Date()).toISOString() + args.createdAt = new Date().toISOString() return resolve(root, args, context, info) } const setUpdatedAt = (resolve, root, args, context, info) => { - args.updatedAt = (new Date()).toISOString() + args.updatedAt = new Date().toISOString() return resolve(root, args, context, info) } @@ -18,6 +18,6 @@ export default { UpdatePost: setUpdatedAt, UpdateComment: setUpdatedAt, UpdateOrganization: setUpdatedAt, - UpdateNotification: setUpdatedAt - } + UpdateNotification: setUpdatedAt, + }, } diff --git a/backend/src/middleware/excerptMiddleware.js b/backend/src/middleware/excerptMiddleware.js index 544dc3529..3b3a27c2c 100644 --- a/backend/src/middleware/excerptMiddleware.js +++ b/backend/src/middleware/excerptMiddleware.js @@ -31,6 +31,6 @@ export default { args.descriptionExcerpt = trunc(args.description, 120).html const result = await resolve(root, args, context, info) return result - } - } + }, + }, } diff --git a/backend/src/middleware/filterBubble/filterBubble.spec.js b/backend/src/middleware/filterBubble/filterBubble.spec.js new file mode 100644 index 000000000..62addeece --- /dev/null +++ b/backend/src/middleware/filterBubble/filterBubble.spec.js @@ -0,0 +1,77 @@ +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../../jest/helpers' +import Factory from '../../seed/factories' + +const factory = Factory() + +const currentUserParams = { + id: 'u1', + email: 'you@example.org', + name: 'This is you', + password: '1234', +} +const followedAuthorParams = { + id: 'u2', + email: 'followed@example.org', + name: 'Followed User', + password: '1234', +} +const randomAuthorParams = { + email: 'someone@example.org', + name: 'Someone else', + password: 'else', +} + +beforeEach(async () => { + await Promise.all([ + factory.create('User', currentUserParams), + factory.create('User', followedAuthorParams), + factory.create('User', randomAuthorParams), + ]) + const [asYourself, asFollowedUser, asSomeoneElse] = await Promise.all([ + Factory().authenticateAs(currentUserParams), + Factory().authenticateAs(followedAuthorParams), + Factory().authenticateAs(randomAuthorParams), + ]) + await asYourself.follow({ id: 'u2', type: 'User' }) + await asFollowedUser.create('Post', { title: 'This is the post of a followed user' }) + await asSomeoneElse.create('Post', { title: 'This is some random post' }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('Filter posts by author is followed by sb.', () => { + describe('given an authenticated user', () => { + let authenticatedClient + + beforeEach(async () => { + const headers = await login(currentUserParams) + authenticatedClient = new GraphQLClient(host, { headers }) + }) + + describe('no filter bubble', () => { + it('returns all posts', async () => { + const query = '{ Post(filter: { }) { title } }' + const expected = { + Post: [ + { title: 'This is some random post' }, + { title: 'This is the post of a followed user' }, + ], + } + await expect(authenticatedClient.request(query)).resolves.toEqual(expected) + }) + }) + + describe('filtering for posts of followed users only', () => { + it('returns only posts authored by followed users', async () => { + const query = '{ Post( filter: { author: { followedBy_some: { id: "u1" } } }) { title } }' + const expected = { + Post: [{ title: 'This is the post of a followed user' }], + } + await expect(authenticatedClient.request(query)).resolves.toEqual(expected) + }) + }) + }) +}) diff --git a/backend/src/middleware/fixImageUrlsMiddleware.js b/backend/src/middleware/fixImageUrlsMiddleware.js index e5dc47a6d..c930915bf 100644 --- a/backend/src/middleware/fixImageUrlsMiddleware.js +++ b/backend/src/middleware/fixImageUrlsMiddleware.js @@ -1,21 +1,24 @@ - const legacyUrls = [ 'https://api-alpha.human-connection.org', 'https://staging-api.human-connection.org', - 'http://localhost:3000' + 'http://localhost:3000', ] -export const fixUrl = (url) => { - legacyUrls.forEach((legacyUrl) => { +export const fixUrl = url => { + legacyUrls.forEach(legacyUrl => { url = url.replace(legacyUrl, '/api') }) return url } -const checkUrl = (thing) => { - return thing && typeof thing === 'string' && legacyUrls.find((legacyUrl) => { - return thing.indexOf(legacyUrl) === 0 - }) +const checkUrl = thing => { + return ( + thing && + typeof thing === 'string' && + legacyUrls.find(legacyUrl => { + return thing.indexOf(legacyUrl) === 0 + }) + ) } export const fixImageURLs = (result, recursive) => { @@ -41,5 +44,5 @@ export default { Query: async (resolve, root, args, context, info) => { let result = await resolve(root, args, context, info) return fixImageURLs(result) - } + }, } diff --git a/backend/src/middleware/fixImageUrlsMiddleware.spec.js b/backend/src/middleware/fixImageUrlsMiddleware.spec.js index 89d2a520d..b2d808dd9 100644 --- a/backend/src/middleware/fixImageUrlsMiddleware.spec.js +++ b/backend/src/middleware/fixImageUrlsMiddleware.spec.js @@ -3,15 +3,21 @@ import { fixImageURLs } from './fixImageUrlsMiddleware' describe('fixImageURLs', () => { describe('image url of legacy alpha', () => { it('removes domain', () => { - const url = 'https://api-alpha.human-connection.org/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png' - expect(fixImageURLs(url)).toEqual('/api/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png') + const url = + 'https://api-alpha.human-connection.org/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png' + expect(fixImageURLs(url)).toEqual( + '/api/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png', + ) }) }) describe('image url of legacy staging', () => { it('removes domain', () => { - const url = 'https://staging-api.human-connection.org/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg' - expect(fixImageURLs(url)).toEqual('/api/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg') + const url = + 'https://staging-api.human-connection.org/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg' + expect(fixImageURLs(url)).toEqual( + '/api/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg', + ) }) }) @@ -24,7 +30,7 @@ describe('fixImageURLs', () => { describe('some string', () => { it('returns untouched', () => {}) - const string = 'Yeah I\'m a String' + const string = "Yeah I'm a String" expect(fixImageURLs(string)).toEqual(string) }) }) diff --git a/backend/src/middleware/includedFieldsMiddleware.js b/backend/src/middleware/includedFieldsMiddleware.js index 5dd63cd3c..cd7a74f4e 100644 --- a/backend/src/middleware/includedFieldsMiddleware.js +++ b/backend/src/middleware/includedFieldsMiddleware.js @@ -2,21 +2,21 @@ import cloneDeep from 'lodash/cloneDeep' const _includeFieldsRecursively = (selectionSet, includedFields) => { if (!selectionSet) return - includedFields.forEach((includedField) => { + includedFields.forEach(includedField => { selectionSet.selections.unshift({ kind: 'Field', - name: { kind: 'Name', value: includedField } + name: { kind: 'Name', value: includedField }, }) }) - selectionSet.selections.forEach((selection) => { + selectionSet.selections.forEach(selection => { _includeFieldsRecursively(selection.selectionSet, includedFields) }) } -const includeFieldsRecursively = (includedFields) => { +const includeFieldsRecursively = includedFields => { return (resolve, root, args, context, resolveInfo) => { const copy = cloneDeep(resolveInfo) - copy.fieldNodes.forEach((fieldNode) => { + copy.fieldNodes.forEach(fieldNode => { _includeFieldsRecursively(fieldNode.selectionSet, includedFields) }) return resolve(root, args, context, copy) @@ -25,5 +25,5 @@ const includeFieldsRecursively = (includedFields) => { export default { Query: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted']), - Mutation: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted']) + Mutation: includeFieldsRecursively(['id', 'createdAt', 'disabled', 'deleted']), } diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 17b9d63fb..75314abc0 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -1,41 +1,63 @@ -import activityPubMiddleware from './activityPubMiddleware' -import passwordMiddleware from './passwordMiddleware' -import softDeleteMiddleware from './softDeleteMiddleware' -import sluggifyMiddleware from './sluggifyMiddleware' -import fixImageUrlsMiddleware from './fixImageUrlsMiddleware' -import excerptMiddleware from './excerptMiddleware' -import dateTimeMiddleware from './dateTimeMiddleware' -import xssMiddleware from './xssMiddleware' -import permissionsMiddleware from './permissionsMiddleware' -import userMiddleware from './userMiddleware' -import includedFieldsMiddleware from './includedFieldsMiddleware' -import orderByMiddleware from './orderByMiddleware' -import validationMiddleware from './validation' -import notificationsMiddleware from './notifications' +import CONFIG from './../config' +import activityPub from './activityPubMiddleware' +import password from './passwordMiddleware' +import softDelete from './softDeleteMiddleware' +import sluggify from './sluggifyMiddleware' +import fixImageUrls from './fixImageUrlsMiddleware' +import excerpt from './excerptMiddleware' +import dateTime from './dateTimeMiddleware' +import xss from './xssMiddleware' +import permissions from './permissionsMiddleware' +import user from './userMiddleware' +import includedFields from './includedFieldsMiddleware' +import orderBy from './orderByMiddleware' +import validation from './validation' +import notifications from './notifications' export default schema => { - let middleware = [ - passwordMiddleware, - dateTimeMiddleware, - validationMiddleware, - sluggifyMiddleware, - excerptMiddleware, - notificationsMiddleware, - xssMiddleware, - fixImageUrlsMiddleware, - softDeleteMiddleware, - userMiddleware, - includedFieldsMiddleware, - orderByMiddleware + const middlewares = { + permissions: permissions, + activityPub: activityPub, + password: password, + dateTime: dateTime, + validation: validation, + sluggify: sluggify, + excerpt: excerpt, + notifications: notifications, + xss: xss, + fixImageUrls: fixImageUrls, + softDelete: softDelete, + user: user, + includedFields: includedFields, + orderBy: orderBy, + } + + let order = [ + 'permissions', + 'activityPub', + 'password', + 'dateTime', + 'validation', + 'sluggify', + 'excerpt', + 'notifications', + 'xss', + 'fixImageUrls', + 'softDelete', + 'user', + 'includedFields', + 'orderBy', ] // add permisions middleware at the first position (unless we're seeding) - // NOTE: DO NOT SET THE PERMISSION FLAT YOUR SELF - if (process.env.NODE_ENV !== 'production') { - const DISABLED_MIDDLEWARES = process.env.DISABLED_MIDDLEWARES || '' - const disabled = DISABLED_MIDDLEWARES.split(',') - if (!disabled.includes('activityPub')) middleware.unshift(activityPubMiddleware) - if (!disabled.includes('permissions')) middleware.unshift(permissionsMiddleware.generate(schema)) + if (CONFIG.DISABLED_MIDDLEWARES) { + const disabledMiddlewares = CONFIG.DISABLED_MIDDLEWARES.split(',') + order = order.filter(key => { + return !disabledMiddlewares.includes(key) + }) + /* eslint-disable-next-line no-console */ + console.log(`Warning: "${disabledMiddlewares}" middlewares have been disabled.`) } - return middleware + + return order.map(key => middlewares[key]) } diff --git a/backend/src/middleware/nodes/locations.js b/backend/src/middleware/nodes/locations.js index 735b047dd..62d1e3a65 100644 --- a/backend/src/middleware/nodes/locations.js +++ b/backend/src/middleware/nodes/locations.js @@ -1,12 +1,12 @@ - import request from 'request' import { UserInputError } from 'apollo-server' import isEmpty from 'lodash/isEmpty' import asyncForEach from '../../helpers/asyncForEach' +import CONFIG from './../../config' const fetch = url => { return new Promise((resolve, reject) => { - request(url, function (error, response, body) { + request(url, function(error, response, body) { if (error) { reject(error) } else { @@ -16,16 +16,7 @@ const fetch = url => { }) } -const locales = [ - 'en', - 'de', - 'fr', - 'nl', - 'it', - 'es', - 'pt', - 'pl' -] +const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl'] const createLocation = async (session, mapboxData) => { const data = { @@ -39,21 +30,22 @@ const createLocation = async (session, mapboxData) => { namePT: mapboxData.text_pt, namePL: mapboxData.text_pl, type: mapboxData.id.split('.')[0].toLowerCase(), - lat: (mapboxData.center && mapboxData.center.length) ? mapboxData.center[0] : null, - lng: (mapboxData.center && mapboxData.center.length) ? mapboxData.center[1] : null + lat: mapboxData.center && mapboxData.center.length ? mapboxData.center[0] : null, + lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null, } - let query = 'MERGE (l:Location {id: $id}) ' + - 'SET l.name = $nameEN, ' + - 'l.nameEN = $nameEN, ' + - 'l.nameDE = $nameDE, ' + - 'l.nameFR = $nameFR, ' + - 'l.nameNL = $nameNL, ' + - 'l.nameIT = $nameIT, ' + - 'l.nameES = $nameES, ' + - 'l.namePT = $namePT, ' + - 'l.namePL = $namePL, ' + - 'l.type = $type' + let query = + 'MERGE (l:Location {id: $id}) ' + + 'SET l.name = $nameEN, ' + + 'l.nameEN = $nameEN, ' + + 'l.nameDE = $nameDE, ' + + 'l.nameFR = $nameFR, ' + + 'l.nameNL = $nameNL, ' + + 'l.nameIT = $nameIT, ' + + 'l.nameES = $nameES, ' + + 'l.namePT = $namePT, ' + + 'l.namePL = $namePL, ' + + 'l.type = $type' if (data.lat && data.lng) { query += ', l.lat = $lat, l.lng = $lng' @@ -67,8 +59,13 @@ const createOrUpdateLocations = async (userId, locationName, driver) => { if (isEmpty(locationName)) { return } - const mapboxToken = process.env.MAPBOX_TOKEN - const res = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(locationName)}.json?access_token=${mapboxToken}&types=region,place,country&language=${locales.join(',')}`) + const res = await fetch( + `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent( + locationName, + )}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${locales.join( + ',', + )}`, + ) if (!res || !res.features || !res.features[0]) { throw new UserInputError('locationName is invalid') @@ -100,24 +97,29 @@ const createOrUpdateLocations = async (userId, locationName, driver) => { await session.run( 'MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) ' + - 'MERGE (child)<-[:IS_IN]-(parent) ' + - 'RETURN child.id, parent.id', { + 'MERGE (child)<-[:IS_IN]-(parent) ' + + 'RETURN child.id, parent.id', + { parentId: parent.id, - childId: ctx.id - }) + childId: ctx.id, + }, + ) parent = ctx }) } // delete all current locations from user await session.run('MATCH (u:User {id: $userId})-[r:IS_IN]->(l:Location) DETACH DELETE r', { - userId: userId + userId: userId, }) // connect user with location - await session.run('MATCH (u:User {id: $userId}), (l:Location {id: $locationId}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id', { - userId: userId, - locationId: data.id - }) + await session.run( + 'MATCH (u:User {id: $userId}), (l:Location {id: $locationId}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id', + { + userId: userId, + locationId: data.id, + }, + ) session.close() } diff --git a/backend/src/middleware/notifications/extractMentions.js b/backend/src/middleware/notifications/extractIds/index.js similarity index 57% rename from backend/src/middleware/notifications/extractMentions.js rename to backend/src/middleware/notifications/extractIds/index.js index f2b28444f..c2fcf169c 100644 --- a/backend/src/middleware/notifications/extractMentions.js +++ b/backend/src/middleware/notifications/extractIds/index.js @@ -1,13 +1,16 @@ import cheerio from 'cheerio' const ID_REGEX = /\/profile\/([\w\-.!~*'"(),]+)/g -export default function (content) { +export default function(content) { + if (!content) return [] const $ = cheerio.load(content) - const urls = $('.mention').map((_, el) => { - return $(el).attr('href') - }).get() + const urls = $('.mention') + .map((_, el) => { + return $(el).attr('href') + }) + .get() const ids = [] - urls.forEach((url) => { + urls.forEach(url => { let match while ((match = ID_REGEX.exec(url)) != null) { ids.push(match[1]) diff --git a/backend/src/middleware/notifications/extractIds/spec.js b/backend/src/middleware/notifications/extractIds/spec.js new file mode 100644 index 000000000..341c39cec --- /dev/null +++ b/backend/src/middleware/notifications/extractIds/spec.js @@ -0,0 +1,59 @@ +import extractIds from '.' + +describe('extractIds', () => { + describe('content undefined', () => { + it('returns empty array', () => { + expect(extractIds()).toEqual([]) + }) + }) + + describe('searches through links', () => { + it('ignores links without .mention class', () => { + const content = + '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' + expect(extractIds(content)).toEqual([]) + }) + + describe('given a link with .mention class', () => { + it('extracts ids', () => { + const content = + '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' + expect(extractIds(content)).toEqual(['u2', 'u3']) + }) + + describe('handles links', () => { + it('with slug and id', () => { + const content = + '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' + expect(extractIds(content)).toEqual(['u2', 'u3']) + }) + + it('with domains', () => { + const content = + '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' + expect(extractIds(content)).toEqual(['u2', 'u3']) + }) + + it('special characters', () => { + const content = + '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' + expect(extractIds(content)).toEqual(['u!*(),2', 'u.~-3']) + }) + }) + + describe('does not crash if', () => { + it('`href` contains no user id', () => { + const content = + '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' + expect(extractIds(content)).toEqual([]) + }) + + it('`href` is empty or invalid', () => { + const content = + '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' + expect(extractIds(content)).toEqual([]) + }) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/extractMentions.spec.js b/backend/src/middleware/notifications/extractMentions.spec.js deleted file mode 100644 index 625b1d8fe..000000000 --- a/backend/src/middleware/notifications/extractMentions.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import extractIds from './extractMentions' - -describe('extract', () => { - describe('searches through links', () => { - it('ignores links without .mention class', () => { - const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual([]) - }) - - describe('given a link with .mention class', () => { - it('extracts ids', () => { - const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual(['u2', 'u3']) - }) - - describe('handles links', () => { - it('with slug and id', () => { - const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual(['u2', 'u3']) - }) - - it('with domains', () => { - const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual(['u2', 'u3']) - }) - - it('special characters', () => { - const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual(['u!*(),2', 'u.~-3']) - }) - }) - - describe('does not crash if', () => { - it('`href` contains no user id', () => { - const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual([]) - }) - - it('`href` is empty or invalid', () => { - const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual([]) - }) - }) - }) - }) -}) diff --git a/backend/src/middleware/notifications/index.js b/backend/src/middleware/notifications/index.js index 942eb588d..ca460a512 100644 --- a/backend/src/middleware/notifications/index.js +++ b/backend/src/middleware/notifications/index.js @@ -1,4 +1,4 @@ -import extractIds from './extractMentions' +import extractIds from './extractIds' const notify = async (resolve, root, args, context, resolveInfo) => { // extract user ids before xss-middleware removes link classes @@ -8,7 +8,7 @@ const notify = async (resolve, root, args, context, resolveInfo) => { const session = context.driver.session() const { id: postId } = post - const createdAt = (new Date()).toISOString() + const createdAt = new Date().toISOString() const cypher = ` match(u:User) where u.id in $ids match(p:Post) where p.id = $postId @@ -25,6 +25,6 @@ const notify = async (resolve, root, args, context, resolveInfo) => { export default { Mutation: { CreatePost: notify, - UpdatePost: notify - } + UpdatePost: notify, + }, } diff --git a/backend/src/middleware/notifications/spec.js b/backend/src/middleware/notifications/spec.js index 786ee7115..65212e544 100644 --- a/backend/src/middleware/notifications/spec.js +++ b/backend/src/middleware/notifications/spec.js @@ -11,7 +11,7 @@ beforeEach(async () => { name: 'Al Capone', slug: 'al-capone', email: 'test@example.org', - password: '1234' + password: '1234', }) }) @@ -47,7 +47,7 @@ describe('currentUser { notifications }', () => { authorParams = { email: 'author@example.org', password: '1234', - id: 'author' + id: 'author', } await factory.create('User', authorParams) authorHeaders = await login(authorParams) @@ -56,7 +56,8 @@ describe('currentUser { notifications }', () => { describe('who mentions me in a post', () => { let post const title = 'Mentioning Al Capone' - const content = 'Hey @al-capone how do you do?' + const content = + 'Hey @al-capone how do you do?' beforeEach(async () => { const createPostMutation = ` @@ -74,20 +75,21 @@ describe('currentUser { notifications }', () => { }) it('sends you a notification', async () => { - const expectedContent = 'Hey @al-capone how do you do?' + const expectedContent = + 'Hey @al-capone how do you do?' const expected = { currentUser: { - notifications: [ - { read: false, post: { content: expectedContent } } - ] - } + notifications: [{ read: false, post: { content: expectedContent } }], + }, } await expect(client.request(query, { read: false })).resolves.toEqual(expected) }) describe('who mentions me again', () => { beforeEach(async () => { - const updatedContent = `${post.content} One more mention to @al-capone` + const updatedContent = `${ + post.content + } One more mention to @al-capone` // The response `post.content` contains a link but the XSSmiddleware // should have the `mention` CSS class removed. I discovered this // during development and thought: A feature not a bug! This way we @@ -106,14 +108,15 @@ describe('currentUser { notifications }', () => { }) it('creates exactly one more notification', async () => { - const expectedContent = 'Hey @al-capone how do you do? One more mention to @al-capone' + const expectedContent = + 'Hey @al-capone how do you do? One more mention to @al-capone' const expected = { currentUser: { notifications: [ { read: false, post: { content: expectedContent } }, - { read: false, post: { content: expectedContent } } - ] - } + { read: false, post: { content: expectedContent } }, + ], + }, } await expect(client.request(query, { read: false })).resolves.toEqual(expected) }) diff --git a/backend/src/middleware/orderByMiddleware.js b/backend/src/middleware/orderByMiddleware.js index 5f8aabb9e..64eac8b74 100644 --- a/backend/src/middleware/orderByMiddleware.js +++ b/backend/src/middleware/orderByMiddleware.js @@ -5,7 +5,7 @@ const defaultOrderBy = (resolve, root, args, context, resolveInfo) => { const newestFirst = { kind: 'Argument', name: { kind: 'Name', value: 'orderBy' }, - value: { kind: 'EnumValue', value: 'createdAt_desc' } + value: { kind: 'EnumValue', value: 'createdAt_desc' }, } const [fieldNode] = copy.fieldNodes if (fieldNode) fieldNode.arguments.push(newestFirst) @@ -14,6 +14,6 @@ const defaultOrderBy = (resolve, root, args, context, resolveInfo) => { export default { Query: { - Post: defaultOrderBy - } + Post: defaultOrderBy, + }, } diff --git a/backend/src/middleware/orderByMiddleware.spec.js b/backend/src/middleware/orderByMiddleware.spec.js index 2d85452e5..450220cd6 100644 --- a/backend/src/middleware/orderByMiddleware.spec.js +++ b/backend/src/middleware/orderByMiddleware.spec.js @@ -1,6 +1,6 @@ +import { GraphQLClient } from 'graphql-request' import Factory from '../seed/factories' import { host } from '../jest/helpers' -import { GraphQLClient } from 'graphql-request' let client let headers @@ -35,7 +35,7 @@ describe('Query', () => { { title: 'last' }, { title: 'third' }, { title: 'second' }, - { title: 'first' } + { title: 'first' }, ] const expected = { Post: posts } await expect(client.request(query)).resolves.toEqual(expected) @@ -51,7 +51,7 @@ describe('Query', () => { { title: 'first' }, { title: 'second' }, { title: 'third' }, - { title: 'last' } + { title: 'last' }, ] const expected = { Post: posts } await expect(client.request(query)).resolves.toEqual(expected) diff --git a/backend/src/middleware/passwordMiddleware.js b/backend/src/middleware/passwordMiddleware.js index 16272421a..1078e5529 100644 --- a/backend/src/middleware/passwordMiddleware.js +++ b/backend/src/middleware/passwordMiddleware.js @@ -8,7 +8,7 @@ export default { const result = await resolve(root, args, context, info) result.password = '*****' return result - } + }, }, Query: async (resolve, root, args, context, info) => { let result = await resolve(root, args, context, info) @@ -17,5 +17,5 @@ export default { return '*****' }) return result - } + }, } diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 85c584407..bc9b4c525 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -1,9 +1,9 @@ import { rule, shield, allow, or } from 'graphql-shield' /* -* TODO: implement -* See: https://github.com/Human-Connection/Nitro-Backend/pull/40#pullrequestreview-180898363 -*/ + * TODO: implement + * See: https://github.com/Human-Connection/Nitro-Backend/pull/40#pullrequestreview-180898363 + */ const isAuthenticated = rule()(async (parent, args, ctx, info) => { return ctx.user !== null }) @@ -13,45 +13,69 @@ const isModerator = rule()(async (parent, args, { user }, info) => { }) const isAdmin = rule()(async (parent, args, { user }, info) => { - return user && (user.role === 'admin') + return user && user.role === 'admin' }) -const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, context, info) => { +const isMyOwn = rule({ + cache: 'no_cache', +})(async (parent, args, context, info) => { return context.user.id === parent.id }) -const belongsToMe = rule({ cache: 'no_cache' })(async (_, args, context) => { - const { driver, user: { id: userId } } = context +const belongsToMe = rule({ + cache: 'no_cache', +})(async (_, args, context) => { + const { + driver, + user: { id: userId }, + } = context const { id: notificationId } = args const session = driver.session() - const result = await session.run(` + const result = await session.run( + ` MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId}) RETURN n - `, { userId, notificationId }) - const [notification] = result.records.map((record) => { + `, + { + userId, + notificationId, + }, + ) + const [notification] = result.records.map(record => { return record.get('n') }) session.close() return Boolean(notification) }) -const onlyEnabledContent = rule({ cache: 'strict' })(async (parent, args, ctx, info) => { +const onlyEnabledContent = rule({ + cache: 'strict', +})(async (parent, args, ctx, info) => { const { disabled, deleted } = args return !(disabled || deleted) }) -const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => { +const isAuthor = rule({ + cache: 'no_cache', +})(async (parent, args, { user, driver }) => { if (!user) return false const session = driver.session() - const { id: postId } = args - const result = await session.run(` - MATCH (post:Post {id: $postId})<-[:WROTE]-(author) + const { id: resourceId } = args + const result = await session.run( + ` + MATCH (resource {id: $resourceId})<-[:WROTE]-(author) RETURN author - `, { postId }) - const [author] = result.records.map((record) => { + `, + { + resourceId, + }, + ) + const [author] = result.records.map(record => { return record.get('author') }) - const { properties: { id: authorId } } = author + const { + properties: { id: authorId }, + } = author session.close() return authorId === user.id }) @@ -62,7 +86,7 @@ const permissions = shield({ Notification: isAdmin, statistics: allow, currentUser: allow, - Post: or(onlyEnabledContent, isModerator) + Post: or(onlyEnabledContent, isModerator), }, Mutation: { UpdateNotification: belongsToMe, @@ -88,14 +112,15 @@ const permissions = shield({ changePassword: isAuthenticated, enable: isModerator, disable: isModerator, - CreateComment: isAuthenticated + CreateComment: isAuthenticated, + DeleteComment: isAuthor, // CreateUser: allow, }, User: { email: isMyOwn, password: isMyOwn, - privateKey: isMyOwn - } + privateKey: isMyOwn, + }, }) export default permissions diff --git a/backend/src/middleware/permissionsMiddleware.spec.js b/backend/src/middleware/permissionsMiddleware.spec.js index e3c4beb00..6cf9dc302 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.js +++ b/backend/src/middleware/permissionsMiddleware.spec.js @@ -1,6 +1,6 @@ +import { GraphQLClient } from 'graphql-request' import Factory from '../seed/factories' import { host, login } from '../jest/helpers' -import { GraphQLClient } from 'graphql-request' const factory = Factory() @@ -10,12 +10,12 @@ describe('authorization', () => { await factory.create('User', { email: 'owner@example.org', name: 'Owner', - password: 'iamtheowner' + password: 'iamtheowner', }) await factory.create('User', { email: 'someone@example.org', name: 'Someone else', - password: 'else' + password: 'else', }) }) @@ -39,14 +39,14 @@ describe('authorization', () => { await expect(action()).rejects.toThrow('Not Authorised!') }) - it('does not expose the owner\'s email address', async () => { + it("does not expose the owner's email address", async () => { let response = {} try { await action() } catch (error) { response = error.response.data } finally { - expect(response).toEqual({ User: [ null ] }) + expect(response).toEqual({ User: [null] }) } }) }) @@ -55,12 +55,12 @@ describe('authorization', () => { beforeEach(() => { loginCredentials = { email: 'owner@example.org', - password: 'iamtheowner' + password: 'iamtheowner', } }) - it('exposes the owner\'s email address', async () => { - await expect(action()).resolves.toEqual({ User: [ { email: 'owner@example.org' } ] }) + it("exposes the owner's email address", async () => { + await expect(action()).resolves.toEqual({ User: [{ email: 'owner@example.org' }] }) }) }) @@ -68,7 +68,7 @@ describe('authorization', () => { beforeEach(async () => { loginCredentials = { email: 'someone@example.org', - password: 'else' + password: 'else', } }) @@ -76,14 +76,14 @@ describe('authorization', () => { await expect(action()).rejects.toThrow('Not Authorised!') }) - it('does not expose the owner\'s email address', async () => { + it("does not expose the owner's email address", async () => { let response try { await action() } catch (error) { response = error.response.data } - expect(response).toEqual({ User: [ null ] }) + expect(response).toEqual({ User: [null] }) }) }) }) diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index c94feb55e..2b1f25d5c 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -3,12 +3,9 @@ import uniqueSlug from './slugify/uniqueSlug' const isUniqueFor = (context, type) => { return async slug => { const session = context.driver.session() - const response = await session.run( - `MATCH(p:${type} {slug: $slug }) return p.slug`, - { - slug - } - ) + const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, { + slug, + }) session.close() return response.records.length === 0 } @@ -17,28 +14,20 @@ const isUniqueFor = (context, type) => { export default { Mutation: { CreatePost: async (resolve, root, args, context, info) => { - args.slug = - args.slug || - (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) + args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) return resolve(root, args, context, info) }, CreateUser: async (resolve, root, args, context, info) => { - args.slug = - args.slug || - (await uniqueSlug(args.name, isUniqueFor(context, 'User'))) + args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User'))) return resolve(root, args, context, info) }, CreateOrganization: async (resolve, root, args, context, info) => { - args.slug = - args.slug || - (await uniqueSlug(args.name, isUniqueFor(context, 'Organization'))) + args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Organization'))) return resolve(root, args, context, info) }, CreateCategory: async (resolve, root, args, context, info) => { - args.slug = - args.slug || - (await uniqueSlug(args.name, isUniqueFor(context, 'Category'))) + args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Category'))) return resolve(root, args, context, info) - } - } + }, + }, } diff --git a/backend/src/middleware/slugify/uniqueSlug.js b/backend/src/middleware/slugify/uniqueSlug.js index 64e38c8ae..69aef2d1b 100644 --- a/backend/src/middleware/slugify/uniqueSlug.js +++ b/backend/src/middleware/slugify/uniqueSlug.js @@ -1,7 +1,7 @@ import slugify from 'slug' -export default async function uniqueSlug (string, isUnique) { +export default async function uniqueSlug(string, isUnique) { let slug = slugify(string || 'anonymous', { - lower: true + lower: true, }) if (await isUnique(slug)) return slug @@ -10,6 +10,6 @@ export default async function uniqueSlug (string, isUnique) { do { count += 1 uniqueSlug = `${slug}-${count}` - } while (!await isUnique(uniqueSlug)) + } while (!(await isUnique(uniqueSlug))) return uniqueSlug } diff --git a/backend/src/middleware/slugify/uniqueSlug.spec.js b/backend/src/middleware/slugify/uniqueSlug.spec.js index 6772a20c2..e34af86a1 100644 --- a/backend/src/middleware/slugify/uniqueSlug.spec.js +++ b/backend/src/middleware/slugify/uniqueSlug.spec.js @@ -3,14 +3,14 @@ import uniqueSlug from './uniqueSlug' describe('uniqueSlug', () => { it('slugifies given string', () => { const string = 'Hello World' - const isUnique = jest.fn() - .mockResolvedValue(true) + const isUnique = jest.fn().mockResolvedValue(true) expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world') }) it('increments slugified string until unique', () => { const string = 'Hello World' - const isUnique = jest.fn() + const isUnique = jest + .fn() .mockResolvedValueOnce(false) .mockResolvedValueOnce(true) expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world-1') @@ -18,8 +18,7 @@ describe('uniqueSlug', () => { it('slugify null string', () => { const string = null - const isUnique = jest.fn() - .mockResolvedValue(true) + const isUnique = jest.fn().mockResolvedValue(true) expect(uniqueSlug(string, isUnique)).resolves.toEqual('anonymous') }) }) diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 6e667056c..79bba0a5d 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -1,6 +1,6 @@ +import { GraphQLClient } from 'graphql-request' import Factory from '../seed/factories' import { host, login } from '../jest/helpers' -import { GraphQLClient } from 'graphql-request' let authenticatedClient let headers @@ -10,7 +10,7 @@ beforeEach(async () => { await factory.create('User', { email: 'user@example.org', password: '1234' }) await factory.create('User', { email: 'someone@example.org', - password: '1234' + password: '1234', }) headers = await login({ email: 'user@example.org', password: '1234' }) authenticatedClient = new GraphQLClient(host, { headers }) @@ -30,7 +30,7 @@ describe('slugify', () => { ) { slug } }`) expect(response).toEqual({ - CreatePost: { slug: 'i-am-a-brand-new-post' } + CreatePost: { slug: 'i-am-a-brand-new-post' }, }) }) @@ -38,11 +38,11 @@ describe('slugify', () => { beforeEach(async () => { const asSomeoneElse = await Factory().authenticateAs({ email: 'someone@example.org', - password: '1234' + password: '1234', }) await asSomeoneElse.create('Post', { title: 'Pre-existing post', - slug: 'pre-existing-post' + slug: 'pre-existing-post', }) }) @@ -54,7 +54,7 @@ describe('slugify', () => { ) { slug } }`) expect(response).toEqual({ - CreatePost: { slug: 'pre-existing-post-1' } + CreatePost: { slug: 'pre-existing-post-1' }, }) }) @@ -67,7 +67,7 @@ describe('slugify', () => { content: "Some content", slug: "pre-existing-post" ) { slug } - }`) + }`), ).rejects.toThrow('already exists') }) }) @@ -81,32 +81,26 @@ describe('slugify', () => { }`) } it('generates a slug based on name', async () => { - await expect( - action('CreateUser', 'name: "I am a user"') - ).resolves.toEqual({ CreateUser: { slug: 'i-am-a-user' } }) + await expect(action('CreateUser', 'name: "I am a user"')).resolves.toEqual({ + CreateUser: { slug: 'i-am-a-user' }, + }) }) describe('if slug exists', () => { beforeEach(async () => { - await action( - 'CreateUser', - 'name: "Pre-existing user", slug: "pre-existing-user"' - ) + await action('CreateUser', 'name: "Pre-existing user", slug: "pre-existing-user"') }) it('chooses another slug', async () => { - await expect( - action('CreateUser', 'name: "pre-existing-user"') - ).resolves.toEqual({ CreateUser: { slug: 'pre-existing-user-1' } }) + await expect(action('CreateUser', 'name: "pre-existing-user"')).resolves.toEqual({ + CreateUser: { slug: 'pre-existing-user-1' }, + }) }) describe('but if the client specifies a slug', () => { it('rejects CreateUser', async () => { await expect( - action( - 'CreateUser', - 'name: "Pre-existing user", slug: "pre-existing-user"' - ) + action('CreateUser', 'name: "Pre-existing user", slug: "pre-existing-user"'), ).rejects.toThrow('already exists') }) }) diff --git a/backend/src/middleware/softDeleteMiddleware.js b/backend/src/middleware/softDeleteMiddleware.js index 53beca219..cc5aa06c5 100644 --- a/backend/src/middleware/softDeleteMiddleware.js +++ b/backend/src/middleware/softDeleteMiddleware.js @@ -30,7 +30,7 @@ export default { Query: { Post: setDefaultFilters, Comment: setDefaultFilters, - User: setDefaultFilters + User: setDefaultFilters, }, Mutation: async (resolve, root, args, context, info) => { args.disabled = false @@ -42,5 +42,5 @@ export default { }, Post: obfuscateDisabled, User: obfuscateDisabled, - Comment: obfuscateDisabled + Comment: obfuscateDisabled, } diff --git a/backend/src/middleware/softDeleteMiddleware.spec.js b/backend/src/middleware/softDeleteMiddleware.spec.js index f007888ed..388f44a3c 100644 --- a/backend/src/middleware/softDeleteMiddleware.spec.js +++ b/backend/src/middleware/softDeleteMiddleware.spec.js @@ -1,6 +1,6 @@ +import { GraphQLClient } from 'graphql-request' import Factory from '../seed/factories' import { host, login } from '../jest/helpers' -import { GraphQLClient } from 'graphql-request' const factory = Factory() let client @@ -11,32 +11,51 @@ beforeAll(async () => { // For performance reasons we do this only once await Promise.all([ factory.create('User', { id: 'u1', role: 'user', email: 'user@example.org', password: '1234' }), - factory.create('User', { id: 'm1', role: 'moderator', email: 'moderator@example.org', password: '1234' }), - factory.create('User', { id: 'u2', role: 'user', name: 'Offensive Name', avatar: '/some/offensive/avatar.jpg', about: 'This self description is very offensive', email: 'troll@example.org', password: '1234' }) + factory.create('User', { + id: 'm1', + role: 'moderator', + email: 'moderator@example.org', + password: '1234', + }), + factory.create('User', { + id: 'u2', + role: 'user', + name: 'Offensive Name', + avatar: '/some/offensive/avatar.jpg', + about: 'This self description is very offensive', + email: 'troll@example.org', + password: '1234', + }), ]) await factory.authenticateAs({ email: 'user@example.org', password: '1234' }) await Promise.all([ factory.follow({ id: 'u2', type: 'User' }), factory.create('Post', { id: 'p1', title: 'Deleted post', deleted: true }), - factory.create('Post', { id: 'p3', title: 'Publicly visible post', deleted: false }) + factory.create('Post', { id: 'p3', title: 'Publicly visible post', deleted: false }), ]) await Promise.all([ - factory.create('Comment', { id: 'c2', postId: 'p3', content: 'Enabled comment on public post' }) + factory.create('Comment', { + id: 'c2', + postId: 'p3', + content: 'Enabled comment on public post', + }), ]) - await Promise.all([ - factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' }) - ]) + await Promise.all([factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' })]) const asTroll = Factory() await asTroll.authenticateAs({ email: 'troll@example.org', password: '1234' }) - await asTroll.create('Post', { id: 'p2', title: 'Disabled post', content: 'This is an offensive post content', image: '/some/offensive/image.jpg', deleted: false }) + await asTroll.create('Post', { + id: 'p2', + title: 'Disabled post', + content: 'This is an offensive post content', + image: '/some/offensive/image.jpg', + deleted: false, + }) await asTroll.create('Comment', { id: 'c1', postId: 'p3', content: 'Disabled comment' }) - await Promise.all([ - asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' }) - ]) + await Promise.all([asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' })]) const asModerator = Factory() await asModerator.authenticateAs({ email: 'moderator@example.org', password: '1234' }) @@ -65,7 +84,8 @@ describe('softDeleteMiddleware', () => { user = response.User[0].following[0] } const beforePost = async () => { - query = '{ User(id: "u1") { following { contributions { title image content contentExcerpt } } } }' + query = + '{ User(id: "u1") { following { contributions { title image content contentExcerpt } } } }' const response = await action() post = response.User[0].following[0].contributions[0] } @@ -84,7 +104,8 @@ describe('softDeleteMiddleware', () => { beforeEach(beforeUser) it('displays name', () => expect(user.name).toEqual('Offensive Name')) - it('displays about', () => expect(user.about).toEqual('This self description is very offensive')) + it('displays about', () => + expect(user.about).toEqual('This self description is very offensive')) it('displays avatar', () => expect(user.avatar).toEqual('/some/offensive/avatar.jpg')) }) @@ -92,8 +113,10 @@ describe('softDeleteMiddleware', () => { beforeEach(beforePost) it('displays title', () => expect(post.title).toEqual('Disabled post')) - it('displays content', () => expect(post.content).toEqual('This is an offensive post content')) - it('displays contentExcerpt', () => expect(post.contentExcerpt).toEqual('This is an offensive post content')) + it('displays content', () => + expect(post.content).toEqual('This is an offensive post content')) + it('displays contentExcerpt', () => + expect(post.contentExcerpt).toEqual('This is an offensive post content')) it('displays image', () => expect(post.image).toEqual('/some/offensive/image.jpg')) }) @@ -101,7 +124,8 @@ describe('softDeleteMiddleware', () => { beforeEach(beforeComment) it('displays content', () => expect(comment.content).toEqual('Disabled comment')) - it('displays contentExcerpt', () => expect(comment.contentExcerpt).toEqual('Disabled comment')) + it('displays contentExcerpt', () => + expect(comment.contentExcerpt).toEqual('Disabled comment')) }) }) @@ -162,10 +186,7 @@ describe('softDeleteMiddleware', () => { }) it('shows disabled but hides deleted posts', async () => { - const expected = [ - { title: 'Disabled post' }, - { title: 'Publicly visible post' } - ] + const expected = [{ title: 'Disabled post' }, { title: 'Publicly visible post' }] const { Post } = await action() await expect(Post).toEqual(expect.arrayContaining(expected)) }) @@ -185,9 +206,11 @@ describe('softDeleteMiddleware', () => { it('conceals disabled comments', async () => { const expected = [ { content: 'Enabled comment on public post' }, - { content: 'UNAVAILABLE' } + { content: 'UNAVAILABLE' }, ] - const { Post: [{ comments }] } = await action() + const { + Post: [{ comments }], + } = await action() await expect(comments).toEqual(expect.arrayContaining(expected)) }) }) @@ -201,9 +224,11 @@ describe('softDeleteMiddleware', () => { it('shows disabled comments', async () => { const expected = [ { content: 'Enabled comment on public post' }, - { content: 'Disabled comment' } + { content: 'Disabled comment' }, ] - const { Post: [{ comments }] } = await action() + const { + Post: [{ comments }], + } = await action() await expect(comments).toEqual(expect.arrayContaining(expected)) }) }) diff --git a/backend/src/middleware/userMiddleware.js b/backend/src/middleware/userMiddleware.js index b3fc1bf2c..29e512ebd 100644 --- a/backend/src/middleware/userMiddleware.js +++ b/backend/src/middleware/userMiddleware.js @@ -1,9 +1,5 @@ -import dotenv from 'dotenv' - import createOrUpdateLocations from './nodes/locations' -dotenv.config() - export default { Mutation: { CreateUser: async (resolve, root, args, context, info) => { @@ -15,6 +11,6 @@ export default { const result = await resolve(root, args, context, info) await createOrUpdateLocations(args.id, args.locationName, context.driver) return result - } - } + }, + }, } diff --git a/backend/src/middleware/validation/index.js b/backend/src/middleware/validation/index.js index 2da25a605..ec69b9a3a 100644 --- a/backend/src/middleware/validation/index.js +++ b/backend/src/middleware/validation/index.js @@ -3,13 +3,11 @@ import { UserInputError } from 'apollo-server' const USERNAME_MIN_LENGTH = 3 const validateUsername = async (resolve, root, args, context, info) => { - if (args.name && args.name.length >= USERNAME_MIN_LENGTH) { + if (!('name' in args) || (args.name && args.name.length >= USERNAME_MIN_LENGTH)) { /* eslint-disable-next-line no-return-await */ return await resolve(root, args, context, info) } else { - throw new UserInputError( - `Username must be at least ${USERNAME_MIN_LENGTH} characters long!` - ) + throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} characters long!`) } } @@ -28,9 +26,7 @@ const validateComment = async (resolve, root, args, context, info) => { const COMMENT_MIN_LENGTH = 1 const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim() if (!args.content || content.length < COMMENT_MIN_LENGTH) { - throw new UserInputError( - `Comment must be at least ${COMMENT_MIN_LENGTH} character long!` - ) + throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) } const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!' const { postId } = args @@ -47,6 +43,6 @@ export default { UpdateUser: validateUsername, CreateSocialMedia: validateUrl, CreateComment: validateComment, - UpdateComment: validateComment - } + UpdateComment: validateComment, + }, } diff --git a/backend/src/middleware/xssMiddleware.js b/backend/src/middleware/xssMiddleware.js index 2607f4210..06aa5b306 100644 --- a/backend/src/middleware/xssMiddleware.js +++ b/backend/src/middleware/xssMiddleware.js @@ -5,7 +5,7 @@ import sanitizeHtml from 'sanitize-html' import cheerio from 'cheerio' import linkifyHtml from 'linkifyjs/html' -const embedToAnchor = (content) => { +const embedToAnchor = content => { const $ = cheerio.load(content) $('div[data-url-embed]').each((i, el) => { let url = el.attribs['data-url-embed'] @@ -15,7 +15,7 @@ const embedToAnchor = (content) => { return $('body').html() } -function clean (dirty) { +function clean(dirty) { if (!dirty) { return dirty } @@ -24,27 +24,48 @@ function clean (dirty) { dirty = embedToAnchor(dirty) dirty = linkifyHtml(dirty) dirty = sanitizeHtml(dirty, { - allowedTags: ['iframe', 'img', 'p', 'h3', 'h4', 'br', 'hr', 'b', 'i', 'em', 'strong', 'a', 'pre', 'ul', 'li', 'ol', 's', 'strike', 'span', 'blockquote'], + allowedTags: [ + 'iframe', + 'img', + 'p', + 'h3', + 'h4', + 'br', + 'hr', + 'b', + 'i', + 'em', + 'strong', + 'a', + 'pre', + 'ul', + 'li', + 'ol', + 's', + 'strike', + 'span', + 'blockquote', + ], allowedAttributes: { a: ['href', 'class', 'target', 'data-*', 'contenteditable'], span: ['contenteditable', 'class', 'data-*'], img: ['src'], - iframe: ['src', 'class', 'frameborder', 'allowfullscreen'] + iframe: ['src', 'class', 'frameborder', 'allowfullscreen'], }, allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'], parser: { - lowerCaseTags: true + lowerCaseTags: true, }, transformTags: { - iframe: function (tagName, attribs) { + iframe: function(tagName, attribs) { return { tagName: 'a', text: attribs.src, attribs: { href: attribs.src, target: '_blank', - 'data-url-embed': '' - } + 'data-url-embed': '', + }, } }, h1: 'h3', @@ -53,19 +74,19 @@ function clean (dirty) { h4: 'h4', h5: 'strong', i: 'em', - a: function (tagName, attribs) { + a: function(tagName, attribs) { return { tagName: 'a', attribs: { href: attribs.href, target: '_blank', - rel: 'noopener noreferrer nofollow' - } + rel: 'noopener noreferrer nofollow', + }, } }, b: 'strong', s: 'strike', - img: function (tagName, attribs) { + img: function(tagName, attribs) { let src = attribs.src if (!src) { @@ -88,11 +109,11 @@ function clean (dirty) { tagName: 'img', attribs: { // TODO: use environment variables - src: `http://localhost:3050/images?url=${src}` - } + src: `http://localhost:3050/images?url=${src}`, + }, } - } - } + }, + }, }) // remove empty html tags and duplicated linebreaks and returns @@ -100,10 +121,7 @@ function clean (dirty) { // remove all tags with "space only" .replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '') // remove all iframes - .replace( - /(]*)(>)[^>]*\/*>/gim, - '' - ) + .replace(/(]*)(>)[^>]*\/*>/gim, '') .replace(/[\n]{3,}/gim, '\n\n') .replace(/(\r\n|\n\r|\r|\n)/g, '
$1') @@ -111,15 +129,9 @@ function clean (dirty) { // limit linebreaks to max 2 (equivalent to html "br" linebreak) .replace(/(
\s*){2,}/gim, '
') // remove additional linebreaks after p tags - .replace( - /<\/(p|div|th|tr)>\s*(
\s*)+\s*<(p|div|th|tr)>/gim, - '

' - ) + .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: -![Screenshot of the kubernetes dashboard showing the extrenal ip address](./ip-address.png) +![Screenshot of Digital Ocean dashboard showing external ip address](./ip-address.png) 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 @@ + + + diff --git a/webapp/components/Badges.vue b/webapp/components/Badges.vue index b28412abe..40d323b2d 100644 --- a/webapp/components/Badges.vue +++ b/webapp/components/Badges.vue @@ -1,20 +1,7 @@ @@ -23,14 +10,14 @@ import HcImage from './Image' export default { components: { - HcImage + HcImage, }, props: { badges: { type: Array, - default: () => [] - } - } + default: () => [], + }, + }, } diff --git a/webapp/components/Category/index.spec.js b/webapp/components/Category/index.spec.js index 149f96189..7ce0b7243 100644 --- a/webapp/components/Category/index.spec.js +++ b/webapp/components/Category/index.spec.js @@ -14,8 +14,8 @@ describe('Category', () => { localVue, propsData: { icon, - name - } + name, + }, }) } diff --git a/webapp/components/Category/index.vue b/webapp/components/Category/index.vue index af602d4d0..acc35772a 100644 --- a/webapp/components/Category/index.vue +++ b/webapp/components/Category/index.vue @@ -1,9 +1,6 @@ @@ -13,7 +10,7 @@ export default { name: 'HcCategory', props: { icon: { type: String, required: true }, - name: { type: String, required: true } - } + name: { type: String, default: '' }, + }, } diff --git a/webapp/components/Comment.spec.js b/webapp/components/Comment.spec.js index 83a738956..e899a05e1 100644 --- a/webapp/components/Comment.spec.js +++ b/webapp/components/Comment.spec.js @@ -1,6 +1,5 @@ -import { config, shallowMount, mount, createLocalVue } from '@vue/test-utils' +import { config, shallowMount, createLocalVue } from '@vue/test-utils' import Comment from './Comment.vue' -import Vue from 'vue' import Vuex from 'vuex' import Styleguide from '@human-connection/styleguide' @@ -12,43 +11,55 @@ localVue.use(Styleguide) config.stubs['no-ssr'] = '' describe('Comment.vue', () => { - let wrapper - let Wrapper let propsData let mocks let getters + let wrapper + let Wrapper beforeEach(() => { propsData = {} mocks = { - $t: jest.fn() + $t: jest.fn(), + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + $apollo: { + mutate: jest.fn().mockResolvedValue(), + }, } getters = { 'auth/user': () => { return {} }, - 'auth/isModerator': () => false + 'auth/isModerator': () => false, } }) describe('shallowMount', () => { - const Wrapper = () => { + Wrapper = () => { const store = new Vuex.Store({ - getters + getters, + }) + return shallowMount(Comment, { + store, + propsData, + mocks, + localVue, }) - return shallowMount(Comment, { store, propsData, mocks, localVue }) } describe('given a comment', () => { beforeEach(() => { propsData.comment = { id: '2', - contentExcerpt: 'Hello I am a comment content' + contentExcerpt: 'Hello I am a comment content', } }) it('renders content', () => { - const wrapper = Wrapper() + wrapper = Wrapper() expect(wrapper.text()).toMatch('Hello I am a comment content') }) @@ -58,17 +69,17 @@ describe('Comment.vue', () => { }) it('renders no comment data', () => { - const wrapper = Wrapper() + wrapper = Wrapper() expect(wrapper.text()).not.toMatch('comment content') }) it('has no "disabled-content" css class', () => { - const wrapper = Wrapper() + wrapper = Wrapper() expect(wrapper.classes()).not.toContain('disabled-content') }) it('translates a placeholder', () => { - const wrapper = Wrapper() + wrapper = Wrapper() const calls = mocks.$t.mock.calls const expected = [['comment.content.unavailable-placeholder']] expect(calls).toEqual(expect.arrayContaining(expected)) @@ -80,16 +91,46 @@ describe('Comment.vue', () => { }) it('renders comment data', () => { - const wrapper = Wrapper() + wrapper = Wrapper() expect(wrapper.text()).toMatch('comment content') }) it('has a "disabled-content" css class', () => { - const wrapper = Wrapper() + wrapper = Wrapper() expect(wrapper.classes()).toContain('disabled-content') }) }) }) + + beforeEach(jest.useFakeTimers) + + describe('test callbacks', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + describe('deletion of Comment from List by invoking "deleteCommentCallback()"', () => { + beforeEach(() => { + wrapper.vm.deleteCommentCallback() + }) + + describe('after timeout', () => { + beforeEach(jest.runAllTimers) + + it('emits "deleteComment"', () => { + expect(wrapper.emitted().deleteComment.length).toBe(1) + }) + + it('does call mutation', () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) + }) + + it('mutation is successful', () => { + expect(mocks.$toast.success).toHaveBeenCalledTimes(1) + }) + }) + }) + }) }) }) }) diff --git a/webapp/components/Comment.vue b/webapp/components/Comment.vue index 906575880..aad840b10 100644 --- a/webapp/components/Comment.vue +++ b/webapp/components/Comment.vue @@ -1,25 +1,20 @@