mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge branch 'master' of github.com:Human-Connection/Human-Connection into 552-update_comment
This commit is contained in:
commit
36c4a22b76
20
.codecov.yml
20
.codecov.yml
@ -95,7 +95,7 @@ coverage:
|
||||
# - master
|
||||
#flags:
|
||||
# - integration
|
||||
paths:
|
||||
paths:
|
||||
- backend/ # only include coverage in "backend/" folder
|
||||
webapp: # declare a new status context "frontend"
|
||||
against: parent
|
||||
@ -127,7 +127,7 @@ coverage:
|
||||
# - integration
|
||||
# paths:
|
||||
# - folder
|
||||
|
||||
|
||||
#changes:
|
||||
# default:
|
||||
# against: parent
|
||||
@ -150,20 +150,8 @@ coverage:
|
||||
|
||||
#ignore: # files and folders for processing
|
||||
# - tests/*
|
||||
|
||||
|
||||
#fixes:
|
||||
# - "old_path::new_path"
|
||||
|
||||
comment:
|
||||
# layout options are quite limited in v4.x - there have been way more options in v1.0
|
||||
layout: reach, diff, flags, files # mostly old options: header, diff, uncovered, reach, files, tree, changes, sunburst, flags
|
||||
behavior: new # default = posts once then update, posts new if delete
|
||||
# once = post once then updates
|
||||
# new = delete old, post new
|
||||
# spammy = post new
|
||||
require_changes: false # if true: only post the comment if coverage changes
|
||||
require_base: no # [yes :: must have a base report to post]
|
||||
require_head: no # [yes :: must have a head report to post]
|
||||
branches: null # branch names that can post comment
|
||||
flags: null
|
||||
paths: null
|
||||
comment: off
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,3 +16,4 @@ cypress.env.json
|
||||
|
||||
!.gitkeep
|
||||
**/coverage
|
||||
|
||||
|
||||
10
.travis.yml
10
.travis.yml
@ -11,7 +11,6 @@ addons:
|
||||
before_install:
|
||||
- yarn global add wait-on
|
||||
# Install Codecov
|
||||
- yarn global add codecov
|
||||
- yarn install
|
||||
- cp cypress.env.template.json cypress.env.json
|
||||
|
||||
@ -29,9 +28,10 @@ script:
|
||||
- docker-compose exec backend yarn run test:jest --ci --verbose=false --coverage
|
||||
- docker-compose exec backend yarn run db:reset
|
||||
- docker-compose exec backend yarn run db:seed
|
||||
- docker-compose exec backend yarn run test:cucumber --tags "not @wip"
|
||||
- docker-compose exec backend yarn run db:reset
|
||||
- docker-compose exec backend yarn run db:seed
|
||||
# ActivityPub cucumber testing temporarily disabled because it's too buggy
|
||||
# - docker-compose exec backend yarn run test:cucumber --tags "not @wip"
|
||||
# - docker-compose exec backend yarn run db:reset
|
||||
# - docker-compose exec backend yarn run db:seed
|
||||
# Frontend
|
||||
- docker-compose exec webapp yarn run lint
|
||||
- docker-compose exec webapp yarn run test --ci --verbose=false --coverage
|
||||
@ -39,7 +39,7 @@ script:
|
||||
# Fullstack
|
||||
- yarn run cypress:run
|
||||
# Coverage
|
||||
- codecov
|
||||
- yarn run codecov
|
||||
|
||||
after_success:
|
||||
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
|
||||
|
||||
@ -4,9 +4,9 @@ Thanks so much for thinking of contributing to the Human Connection project, we
|
||||
|
||||
## Getting Set Up
|
||||
|
||||
Instructions for how to install all the necessary software can be found in our [documentation](https://docs.human-connection.org/human-connection/)
|
||||
Instructions for how to install all the necessary software can be found in our [documentation](https://docs.human-connection.org/human-connection/).
|
||||
|
||||
We recommend that new folks should ideally work together with an existing developer. Please join our discord instance to chat with developers or just ask them in tickets in [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f/boards?repos=152252353):
|
||||
We recommend that new folks should ideally work together with an existing developer. Please join our [discord](https://discord.gg/6ub73U3) instance to chat with developers or just ask them in tickets in [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f/boards?repos=152252353):
|
||||
|
||||

|
||||
|
||||
@ -17,7 +17,7 @@ Here are some general notes on our development flow:
|
||||
* Currently operating in two week sprints
|
||||
* We are using ZenHub to coordinate
|
||||
* estimating time per issue is the crucial feature of [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f) that Github does not have
|
||||
* "up-for-grabs" links to [Github project](https://github.com/orgs/Human-Connection/projects/10?card_filter_query=label%3A"good+first+issue)
|
||||
* "up-for-grabs" links to [Github project](https://github.com/Human-Connection/Human-Connection/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||
* ordering on ZenHub not necessarily reflected on github projects
|
||||
* AgileVentures run open pairing sessions at 10:30am UTC each week on Tuesdays and Thursdays
|
||||
* Core team
|
||||
@ -51,19 +51,19 @@ But what do we do when waiting for merge into master \(wanting to keep PRs small
|
||||
* solutions
|
||||
* 1\) put 2nd PR into branch that the first PR is hitting - but requires update after merging
|
||||
* 2\) prefer to leave exiting PR until it can be reviewed, and instead go and work on some other part of the codebase that is not impacted by the first PR
|
||||
|
||||
|
||||
### Code Review
|
||||
* Github setting in place - at least one review is required to merge
|
||||
* Github setting in place - at least one review is required to merge
|
||||
- in principle anyone (who is not the PR owner) can review
|
||||
- but often it will be the core developers (Robert, Ulf, Greg, Wolfgang?)
|
||||
- once there is a review, and presuming no requested changes, PR opener can merge
|
||||
|
||||
* CI/tests
|
||||
- the CI needs to pass
|
||||
- the CI needs to pass
|
||||
- linting <-- autofix?
|
||||
- tests (unit, feature) (backend, frontend)
|
||||
- codecoverage
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
question: when you want to pick a task - \(find out priority\) - is it in discord? is it in AV slack? --> Robert says you can always ask in discord - group channels are the best
|
||||
@ -77,4 +77,3 @@ Matt makes point that new stories will have to be taken off the "New Issues" and
|
||||
Robert notes that everyone is invited to join the kickoff meetings
|
||||
|
||||
Robert - difference between "important" \(creates a lot of value\) and "beginner friendly" \(easy to implement\)
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ Thank you lokalise for providing us with a premium account :raised_hands:.
|
||||
## Developer Chat
|
||||
|
||||
Join our friendly open-source community on [Discord](https://discord.gg/6ub73U3) :heart_eyes_cat:
|
||||
Just introduce yourself at `#user-presentation` and mention `@@Mentor` to get you onboard :neckbeard:
|
||||
Just introduce yourself at `#introduce-yourself` and mention `@@Mentor` to get you onboard :neckbeard:
|
||||
Check out the [contribution guideline](./CONTRIBUTING.md), too!
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
NEO4J_URI=bolt://localhost:7687
|
||||
NEO4J_USER=neo4j
|
||||
NEO4J_USERNAME=neo4j
|
||||
NEO4J_PASSWORD=letmein
|
||||
GRAPHQL_PORT=4000
|
||||
GRAPHQL_URI=http://localhost:4000
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM node:12.4-alpine as base
|
||||
FROM node:12.6-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
|
||||
|
||||
@ -34,7 +34,6 @@
|
||||
"!**/src/**/?(*.)+(spec|test).js?(x)"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text",
|
||||
"lcov"
|
||||
],
|
||||
"testMatch": [
|
||||
@ -42,63 +41,65 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/joi": "^15.1.0",
|
||||
"activitystrea.ms": "~2.1.3",
|
||||
"apollo-cache-inmemory": "~1.6.2",
|
||||
"apollo-client": "~2.6.3",
|
||||
"apollo-link-context": "~1.0.18",
|
||||
"apollo-link-http": "~1.5.15",
|
||||
"apollo-server": "~2.6.6",
|
||||
"apollo-server": "~2.6.9",
|
||||
"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.37",
|
||||
"date-fns": "2.0.0-beta.1",
|
||||
"debug": "~4.1.1",
|
||||
"dotenv": "~8.0.0",
|
||||
"express": "~4.17.1",
|
||||
"faker": "Marak/faker.js#master",
|
||||
"graphql": "~14.3.1",
|
||||
"graphql": "~14.4.2",
|
||||
"graphql-custom-directives": "~0.2.14",
|
||||
"graphql-iso-date": "~3.6.1",
|
||||
"graphql-middleware": "~3.0.2",
|
||||
"graphql-shield": "~5.7.1",
|
||||
"graphql-shield": "~6.0.3",
|
||||
"graphql-tag": "~2.10.1",
|
||||
"graphql-yoga": "~1.18.0",
|
||||
"helmet": "~3.18.0",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"linkifyjs": "~2.1.8",
|
||||
"lodash": "~4.17.11",
|
||||
"lodash": "~4.17.14",
|
||||
"merge-graphql-schemas": "^1.5.8",
|
||||
"neo4j-driver": "~1.7.4",
|
||||
"neo4j-graphql-js": "^2.6.3",
|
||||
"neode": "^0.2.16",
|
||||
"node-fetch": "~2.6.0",
|
||||
"nodemailer": "^6.2.1",
|
||||
"nodemailer": "^6.3.0",
|
||||
"npm-run-all": "~4.1.5",
|
||||
"request": "~2.88.0",
|
||||
"sanitize-html": "~1.20.1",
|
||||
"slug": "~1.1.0",
|
||||
"trunc-html": "~1.1.2",
|
||||
"uuid": "~3.3.2",
|
||||
"wait-on": "~3.2.0"
|
||||
"wait-on": "~3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "~7.4.4",
|
||||
"@babel/core": "~7.4.5",
|
||||
"@babel/node": "~7.4.5",
|
||||
"@babel/cli": "~7.5.0",
|
||||
"@babel/core": "~7.5.4",
|
||||
"@babel/node": "~7.5.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
||||
"@babel/preset-env": "~7.4.5",
|
||||
"@babel/preset-env": "~7.5.4",
|
||||
"@babel/register": "~7.4.4",
|
||||
"apollo-server-testing": "~2.6.6",
|
||||
"apollo-server-testing": "~2.6.9",
|
||||
"babel-core": "~7.0.0-0",
|
||||
"babel-eslint": "~10.0.2",
|
||||
"babel-jest": "~24.8.0",
|
||||
"chai": "~4.2.0",
|
||||
"cucumber": "~5.1.0",
|
||||
"eslint": "~6.0.1",
|
||||
"eslint-config-prettier": "~5.0.0",
|
||||
"eslint-config-prettier": "~6.0.0",
|
||||
"eslint-config-standard": "~12.0.0",
|
||||
"eslint-plugin-import": "~2.18.0",
|
||||
"eslint-plugin-jest": "~22.7.1",
|
||||
"eslint-plugin-jest": "~22.7.2",
|
||||
"eslint-plugin-node": "~9.1.0",
|
||||
"eslint-plugin-prettier": "~3.1.0",
|
||||
"eslint-plugin-promise": "~4.2.1",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { v1 as neo4j } from 'neo4j-driver'
|
||||
import CONFIG from './../config'
|
||||
import setupNeode from './neode'
|
||||
|
||||
let driver
|
||||
|
||||
@ -14,3 +15,12 @@ export function getDriver(options = {}) {
|
||||
}
|
||||
return driver
|
||||
}
|
||||
|
||||
let neodeInstance
|
||||
export function neode() {
|
||||
if (!neodeInstance) {
|
||||
const { NEO4J_URI: uri, NEO4J_USERNAME: username, NEO4J_PASSWORD: password } = CONFIG
|
||||
neodeInstance = setupNeode({ uri, username, password })
|
||||
}
|
||||
return neodeInstance
|
||||
}
|
||||
|
||||
9
backend/src/bootstrap/neode.js
Normal file
9
backend/src/bootstrap/neode.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Neode from 'neode'
|
||||
import models from '../models'
|
||||
|
||||
export default function setupNeode(options) {
|
||||
const { uri, username, password } = options
|
||||
const neodeInstance = new Neode(uri, username, password)
|
||||
neodeInstance.with(models)
|
||||
return neodeInstance
|
||||
}
|
||||
7
backend/src/helpers/encryptPassword.js
Normal file
7
backend/src/helpers/encryptPassword.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { hashSync } from 'bcryptjs'
|
||||
|
||||
export default function(args) {
|
||||
args.encryptedPassword = hashSync(args.password, 10)
|
||||
delete args.password
|
||||
return args
|
||||
}
|
||||
@ -4,12 +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(variables) {
|
||||
const mutation = `
|
||||
mutation {
|
||||
login(email:"${email}", password:"${password}")
|
||||
}`
|
||||
const response = await request(host, mutation)
|
||||
mutation($email: String!, $password: String!) {
|
||||
login(email: $email, password: $password)
|
||||
}
|
||||
`
|
||||
const response = await request(host, mutation, variables)
|
||||
return {
|
||||
authorization: `Bearer ${response.login}`,
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ export default {
|
||||
}
|
||||
return post
|
||||
},
|
||||
CreateUser: async (resolve, root, args, context, info) => {
|
||||
SignupVerification: async (resolve, root, args, context, info) => {
|
||||
const keys = generateRsaKeyPair()
|
||||
Object.assign(args, keys)
|
||||
args.actorId = `${activityPub.host}/activitypub/users/${args.slug}`
|
||||
|
||||
@ -9,7 +9,6 @@ const setUpdatedAt = (resolve, root, args, context, info) => {
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateUser: setCreatedAt,
|
||||
CreatePost: setCreatedAt,
|
||||
CreateComment: setCreatedAt,
|
||||
CreateOrganization: setCreatedAt,
|
||||
|
||||
57
backend/src/middleware/email/emailMiddleware.js
Normal file
57
backend/src/middleware/email/emailMiddleware.js
Normal file
@ -0,0 +1,57 @@
|
||||
import CONFIG from '../../config'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { resetPasswordMail, wrongAccountMail } from './templates/passwordReset'
|
||||
import { signupTemplate } from './templates/signup'
|
||||
|
||||
const transporter = () => {
|
||||
const configs = {
|
||||
host: CONFIG.SMTP_HOST,
|
||||
port: CONFIG.SMTP_PORT,
|
||||
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
|
||||
secure: false, // true for 465, false for other ports
|
||||
}
|
||||
const { SMTP_USERNAME: user, SMTP_PASSWORD: pass } = CONFIG
|
||||
if (user && pass) {
|
||||
configs.auth = { user, pass }
|
||||
}
|
||||
return nodemailer.createTransport(configs)
|
||||
}
|
||||
|
||||
const returnResponse = async (resolve, root, args, context, resolveInfo) => {
|
||||
const { response } = await resolve(root, args, context, resolveInfo)
|
||||
delete response.nonce
|
||||
return response
|
||||
}
|
||||
|
||||
const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
|
||||
const { email } = args
|
||||
const { response, nonce } = await resolve(root, args, context, resolveInfo)
|
||||
delete response.nonce
|
||||
await transporter().sendMail(signupTemplate({ email, nonce }))
|
||||
return response
|
||||
}
|
||||
|
||||
export default function({ isEnabled }) {
|
||||
if (!isEnabled)
|
||||
return {
|
||||
Mutation: {
|
||||
requestPasswordReset: returnResponse,
|
||||
Signup: returnResponse,
|
||||
SignupByInvitation: returnResponse,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
Mutation: {
|
||||
requestPasswordReset: async (resolve, root, args, context, resolveInfo) => {
|
||||
const { email } = args
|
||||
const { response, user, code, name } = await resolve(root, args, context, resolveInfo)
|
||||
const mailTemplate = user ? resetPasswordMail : wrongAccountMail
|
||||
await transporter().sendMail(mailTemplate({ email, code, name }))
|
||||
return response
|
||||
},
|
||||
Signup: sendSignupMail,
|
||||
SignupByInvitation: sendSignupMail,
|
||||
},
|
||||
}
|
||||
}
|
||||
85
backend/src/middleware/email/templates/passwordReset.js
Normal file
85
backend/src/middleware/email/templates/passwordReset.js
Normal file
@ -0,0 +1,85 @@
|
||||
import CONFIG from '../../../config'
|
||||
|
||||
export const from = '"Human Connection" <info@human-connection.org>'
|
||||
|
||||
export const resetPasswordMail = options => {
|
||||
const {
|
||||
name,
|
||||
email,
|
||||
code,
|
||||
subject = 'Use this link to reset your password. The link is only valid for 24 hours.',
|
||||
supportUrl = 'https://human-connection.org/en/contact/',
|
||||
} = options
|
||||
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
|
||||
actionUrl.searchParams.set('code', code)
|
||||
actionUrl.searchParams.set('email', email)
|
||||
|
||||
return {
|
||||
to: email,
|
||||
subject,
|
||||
text: `
|
||||
Hi ${name}!
|
||||
|
||||
You recently requested to reset your password for your Human Connection account.
|
||||
Use the link below to reset it. This password reset is only valid for the next
|
||||
24 hours.
|
||||
|
||||
${actionUrl}
|
||||
|
||||
If you did not request a password reset, please ignore this email or contact
|
||||
support if you have questions:
|
||||
|
||||
${supportUrl}
|
||||
|
||||
Thanks,
|
||||
The Human Connection Team
|
||||
|
||||
If you're having trouble with the link above, you can manually copy and
|
||||
paste the following code into your browser window:
|
||||
|
||||
${code}
|
||||
|
||||
Human Connection gemeinnützige GmbH
|
||||
Bahnhofstr. 11
|
||||
73235 Weilheim / Teck
|
||||
Deutschland
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
export const wrongAccountMail = options => {
|
||||
const {
|
||||
email,
|
||||
subject = `We received a request to reset your password with this email address (${email})`,
|
||||
supportUrl = 'https://human-connection.org/en/contact/',
|
||||
} = options
|
||||
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
|
||||
return {
|
||||
to: email,
|
||||
subject,
|
||||
text: `
|
||||
We received a request to reset the password to access Human Connection with your
|
||||
email address, but we were unable to find an account associated with this
|
||||
address.
|
||||
|
||||
If you use Human Connection and were expecting this email, consider trying to
|
||||
request a password reset using the email address associated with your account.
|
||||
Try a different email:
|
||||
|
||||
${actionUrl}
|
||||
|
||||
If you do not use Human Connection or did not request a password reset, please
|
||||
ignore this email. Feel free to contact support if you have further questions:
|
||||
|
||||
${supportUrl}
|
||||
|
||||
Thanks,
|
||||
The Human Connection Team
|
||||
|
||||
Human Connection gemeinnützige GmbH
|
||||
Bahnhofstr. 11
|
||||
73235 Weilheim / Teck
|
||||
Deutschland
|
||||
`,
|
||||
}
|
||||
}
|
||||
43
backend/src/middleware/email/templates/signup.js
Normal file
43
backend/src/middleware/email/templates/signup.js
Normal file
@ -0,0 +1,43 @@
|
||||
import CONFIG from '../../../config'
|
||||
|
||||
export const from = '"Human Connection" <info@human-connection.org>'
|
||||
|
||||
export const signupTemplate = options => {
|
||||
const {
|
||||
email,
|
||||
nonce,
|
||||
subject = 'Signup link',
|
||||
supportUrl = 'https://human-connection.org/en/contact/',
|
||||
} = options
|
||||
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
|
||||
actionUrl.searchParams.set('nonce', nonce)
|
||||
actionUrl.searchParams.set('email', email)
|
||||
|
||||
return {
|
||||
to: email,
|
||||
subject,
|
||||
text: `
|
||||
Welcome to Human Connection! Use this link to complete the registration process
|
||||
and create a user account:
|
||||
|
||||
${actionUrl}
|
||||
|
||||
You can also copy+paste this verification code in your browser window:
|
||||
|
||||
${nonce}
|
||||
|
||||
If you did not signed up for Human Connection, please ignore this email or
|
||||
contact support if you have questions:
|
||||
|
||||
${supportUrl}
|
||||
|
||||
Thanks,
|
||||
The Human Connection Team
|
||||
|
||||
Human Connection gemeinnützige GmbH
|
||||
Bahnhofstr. 11
|
||||
73235 Weilheim / Teck
|
||||
Deutschland
|
||||
`,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
import extractMentionedUsers from './notifications/extractMentionedUsers'
|
||||
import extractHashtags from './hashtags/extractHashtags'
|
||||
|
||||
const notify = async (postId, idsOfMentionedUsers, context) => {
|
||||
const session = context.driver.session()
|
||||
const createdAt = new Date().toISOString()
|
||||
const cypher = `
|
||||
match(u:User) where u.id in $idsOfMentionedUsers
|
||||
match(p:Post) where p.id = $postId
|
||||
create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt})
|
||||
merge (n)-[:NOTIFIED]->(u)
|
||||
merge (p)-[:NOTIFIED]->(n)
|
||||
`
|
||||
await session.run(cypher, {
|
||||
idsOfMentionedUsers,
|
||||
createdAt,
|
||||
postId,
|
||||
})
|
||||
session.close()
|
||||
}
|
||||
|
||||
const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||
const session = context.driver.session()
|
||||
// We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement
|
||||
// functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted
|
||||
// and no new Hashtags and relations will be created.
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (p:Post { id: $postId })-[previousRelations:TAGGED]->(t:Tag)
|
||||
DELETE previousRelations
|
||||
RETURN p, t
|
||||
`
|
||||
const cypherCreateNewTagsAndRelations = `
|
||||
MATCH (p:Post { id: $postId})
|
||||
UNWIND $hashtags AS tagName
|
||||
MERGE (t:Tag { id: tagName, name: tagName, disabled: false, deleted: false })
|
||||
MERGE (p)-[:TAGGED]->(t)
|
||||
RETURN p, t
|
||||
`
|
||||
await session.run(cypherDeletePreviousRelations, {
|
||||
postId,
|
||||
})
|
||||
await session.run(cypherCreateNewTagsAndRelations, {
|
||||
postId,
|
||||
hashtags,
|
||||
})
|
||||
session.close()
|
||||
}
|
||||
|
||||
const handleContentData = async (resolve, root, args, context, resolveInfo) => {
|
||||
// extract user ids before xss-middleware removes classes via the following "resolve" call
|
||||
const idsOfMentionedUsers = extractMentionedUsers(args.content)
|
||||
// extract tag (hashtag) ids before xss-middleware removes classes via the following "resolve" call
|
||||
const hashtags = extractHashtags(args.content)
|
||||
|
||||
// removes classes from the content
|
||||
const post = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
await notify(post.id, idsOfMentionedUsers, context)
|
||||
await updateHashtagsOfPost(post.id, hashtags, context)
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: handleContentData,
|
||||
UpdatePost: handleContentData,
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,286 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import gql from 'graphql-tag'
|
||||
import { host, login } from '../../jest/helpers'
|
||||
import Factory from '../../seed/factories'
|
||||
|
||||
const factory = Factory()
|
||||
let client
|
||||
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
id: 'you',
|
||||
name: 'Al Capone',
|
||||
slug: 'al-capone',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('currentUser { notifications }', () => {
|
||||
const query = gql`
|
||||
query($read: Boolean) {
|
||||
currentUser {
|
||||
notifications(read: $read, orderBy: createdAt_desc) {
|
||||
read
|
||||
post {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('authenticated', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
})
|
||||
|
||||
describe('given another user', () => {
|
||||
let authorClient
|
||||
let authorParams
|
||||
let authorHeaders
|
||||
|
||||
beforeEach(async () => {
|
||||
authorParams = {
|
||||
email: 'author@example.org',
|
||||
password: '1234',
|
||||
id: 'author',
|
||||
}
|
||||
await factory.create('User', authorParams)
|
||||
authorHeaders = await login(authorParams)
|
||||
})
|
||||
|
||||
describe('who mentions me in a post', () => {
|
||||
let post
|
||||
const title = 'Mentioning Al Capone'
|
||||
const content =
|
||||
'Hey <a class="mention" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
||||
|
||||
beforeEach(async () => {
|
||||
const createPostMutation = gql`
|
||||
mutation($title: String!, $content: String!) {
|
||||
CreatePost(title: $title, content: $content) {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
authorClient = new GraphQLClient(host, {
|
||||
headers: authorHeaders,
|
||||
})
|
||||
const { CreatePost } = await authorClient.request(createPostMutation, {
|
||||
title,
|
||||
content,
|
||||
})
|
||||
post = CreatePost
|
||||
})
|
||||
|
||||
it('sends you a notification', async () => {
|
||||
const expectedContent =
|
||||
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
|
||||
const expected = {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
await expect(
|
||||
client.request(query, {
|
||||
read: false,
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
describe('who mentions me again', () => {
|
||||
beforeEach(async () => {
|
||||
const updatedContent = `${post.content} One more mention to <a href="/profile/you" class="mention">@al-capone</a>`
|
||||
// The response `post.content` contains a link but the XSSmiddleware
|
||||
// should have the `mention` CSS class removed. I discovered this
|
||||
// during development and thought: A feature not a bug! This way we
|
||||
// can encode a re-mentioning of users when you edit your post or
|
||||
// comment.
|
||||
const updatePostMutation = gql`
|
||||
mutation($id: ID!, $title: String!, $content: String!) {
|
||||
UpdatePost(id: $id, content: $content, title: $title) {
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
authorClient = new GraphQLClient(host, {
|
||||
headers: authorHeaders,
|
||||
})
|
||||
await authorClient.request(updatePostMutation, {
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
content: updatedContent,
|
||||
})
|
||||
})
|
||||
|
||||
it('creates exactly one more notification', async () => {
|
||||
const expectedContent =
|
||||
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do? One more mention to <a href="/profile/you" target="_blank">@al-capone</a>'
|
||||
const expected = {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
},
|
||||
{
|
||||
read: false,
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
await expect(
|
||||
client.request(query, {
|
||||
read: false,
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Hashtags', () => {
|
||||
const postId = 'p135'
|
||||
const postTitle = 'Two Hashtags'
|
||||
const postContent =
|
||||
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Democracy">#Democracy</a> should work equal for everybody!? That seems to be the only way to have equal <a class="hashtag" href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||
const postWithHastagsQuery = gql`
|
||||
query($id: ID) {
|
||||
Post(id: $id) {
|
||||
tags {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const postWithHastagsVariables = {
|
||||
id: postId,
|
||||
}
|
||||
const createPostMutation = gql`
|
||||
mutation($postId: ID, $postTitle: String!, $postContent: String!) {
|
||||
CreatePost(id: $postId, title: $postTitle, content: $postContent) {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('authenticated', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
})
|
||||
|
||||
describe('create a Post with Hashtags', () => {
|
||||
beforeEach(async () => {
|
||||
await client.request(createPostMutation, {
|
||||
postId,
|
||||
postTitle,
|
||||
postContent,
|
||||
})
|
||||
})
|
||||
|
||||
it('both Hashtags are created with the "id" set to thier "name"', async () => {
|
||||
const expected = [
|
||||
{
|
||||
id: 'Democracy',
|
||||
name: 'Democracy',
|
||||
},
|
||||
{
|
||||
id: 'Liberty',
|
||||
name: 'Liberty',
|
||||
},
|
||||
]
|
||||
await expect(
|
||||
client.request(postWithHastagsQuery, postWithHastagsVariables),
|
||||
).resolves.toEqual({
|
||||
Post: [
|
||||
{
|
||||
tags: expect.arrayContaining(expected),
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => {
|
||||
// The already existing Hashtag has no class at this point.
|
||||
const updatedPostContent =
|
||||
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Elections">#Elections</a> should work equal for everybody!? That seems to be the only way to have equal <a href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||
const updatePostMutation = gql`
|
||||
mutation($postId: ID!, $postTitle: String!, $updatedPostContent: String!) {
|
||||
UpdatePost(id: $postId, title: $postTitle, content: $updatedPostContent) {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
it('only one previous Hashtag and the new Hashtag exists', async () => {
|
||||
await client.request(updatePostMutation, {
|
||||
postId,
|
||||
postTitle,
|
||||
updatedPostContent,
|
||||
})
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: 'Elections',
|
||||
name: 'Elections',
|
||||
},
|
||||
{
|
||||
id: 'Liberty',
|
||||
name: 'Liberty',
|
||||
},
|
||||
]
|
||||
await expect(
|
||||
client.request(postWithHastagsQuery, postWithHastagsVariables),
|
||||
).resolves.toEqual({
|
||||
Post: [
|
||||
{
|
||||
tags: expect.arrayContaining(expected),
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,28 @@
|
||||
import cheerio from 'cheerio'
|
||||
// formats of a Hashtag:
|
||||
// https://en.wikipedia.org/w/index.php?title=Hashtag&oldid=905141980#Style
|
||||
// here:
|
||||
// 0. Search for whole string.
|
||||
// 1. Hashtag has only 'a-z', 'A-Z', and '0-9'.
|
||||
// 2. If it starts with a digit '0-9' than 'a-z', or 'A-Z' has to follow.
|
||||
const ID_REGEX = /^\/search\/hashtag\/(([a-zA-Z]+[a-zA-Z0-9]*)|([0-9]+[a-zA-Z]+[a-zA-Z0-9]*))$/g
|
||||
|
||||
export default function(content) {
|
||||
if (!content) return []
|
||||
const $ = cheerio.load(content)
|
||||
// We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware.
|
||||
// But we have to know, which Hashtags are removed from the content es well, so we search for the 'a' html-tag.
|
||||
const urls = $('a')
|
||||
.map((_, el) => {
|
||||
return $(el).attr('href')
|
||||
})
|
||||
.get()
|
||||
const hashtags = []
|
||||
urls.forEach(url => {
|
||||
let match
|
||||
while ((match = ID_REGEX.exec(url)) != null) {
|
||||
hashtags.push(match[1])
|
||||
}
|
||||
})
|
||||
return hashtags
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
import extractHashtags from './extractHashtags'
|
||||
|
||||
describe('extractHashtags', () => {
|
||||
describe('content undefined', () => {
|
||||
it('returns empty array', () => {
|
||||
expect(extractHashtags()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('searches through links', () => {
|
||||
it('finds links with and without ".hashtag" class and extracts Hashtag names', () => {
|
||||
const content =
|
||||
'<p><a class="hashtag" href="/search/hashtag/Elections">#Elections</a><a href="/search/hashtag/Democracy">#Democracy</a></p>'
|
||||
expect(extractHashtags(content)).toEqual(['Elections', 'Democracy'])
|
||||
})
|
||||
|
||||
it('ignores mentions', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractHashtags(content)).toEqual([])
|
||||
})
|
||||
|
||||
describe('handles links', () => {
|
||||
it('ignores links with domains', () => {
|
||||
const content =
|
||||
'<p><a class="hashtag" href="http://localhost:3000/search/hashtag/Elections">#Elections</a><a href="/search/hashtag/Democracy">#Democracy</a></p>'
|
||||
expect(extractHashtags(content)).toEqual(['Democracy'])
|
||||
})
|
||||
|
||||
it('ignores Hashtag links with not allowed character combinations', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/search/hashtag/AbcDefXyz0123456789!*(),2" class="hashtag" target="_blank">#AbcDefXyz0123456789!*(),2</a>, <a href="/search/hashtag/0123456789" class="hashtag" target="_blank">#0123456789</a>, <a href="/search/hashtag/0123456789a" class="hashtag" target="_blank">#0123456789a</a> and <a href="/search/hashtag/AbcDefXyz0123456789" target="_blank">#AbcDefXyz0123456789</a>.</p>'
|
||||
expect(extractHashtags(content)).toEqual(['0123456789a', 'AbcDefXyz0123456789'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('does not crash if', () => {
|
||||
it('`href` contains no Hashtag name', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/search/hashtag/" target="_blank">#Democracy</a> and <a href="/search/hashtag" target="_blank">#liberty</a>.</p>'
|
||||
expect(extractHashtags(content)).toEqual([])
|
||||
})
|
||||
|
||||
it('`href` contains Hashtag as page anchor', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="https://www.example.org/#anchor" target="_blank">#anchor</a>.</p>'
|
||||
expect(extractHashtags(content)).toEqual([])
|
||||
})
|
||||
|
||||
it('`href` is empty or invalid', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="" class="hashtag" target="_blank">@bob-der-baumeister</a> and <a href="not-a-url" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractHashtags(content)).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,9 +1,9 @@
|
||||
import extractIds from '.'
|
||||
import extractMentionedUsers from './extractMentionedUsers'
|
||||
|
||||
describe('extractIds', () => {
|
||||
describe('extractMentionedUsers', () => {
|
||||
describe('content undefined', () => {
|
||||
it('returns empty array', () => {
|
||||
expect(extractIds()).toEqual([])
|
||||
expect(extractMentionedUsers()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@ -11,33 +11,33 @@ describe('extractIds', () => {
|
||||
it('ignores links without .mention class', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractIds(content)).toEqual([])
|
||||
expect(extractMentionedUsers(content)).toEqual([])
|
||||
})
|
||||
|
||||
describe('given a link with .mention class', () => {
|
||||
it('extracts ids', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/profile/u2" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractIds(content)).toEqual(['u2', 'u3'])
|
||||
expect(extractMentionedUsers(content)).toEqual(['u2', 'u3'])
|
||||
})
|
||||
|
||||
describe('handles links', () => {
|
||||
it('with slug and id', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractIds(content)).toEqual(['u2', 'u3'])
|
||||
expect(extractMentionedUsers(content)).toEqual(['u2', 'u3'])
|
||||
})
|
||||
|
||||
it('with domains', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="http://localhost:3000/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractIds(content)).toEqual(['u2', 'u3'])
|
||||
expect(extractMentionedUsers(content)).toEqual(['u2', 'u3'])
|
||||
})
|
||||
|
||||
it('special characters', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="http://localhost:3000/profile/u!*(),2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u.~-3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractIds(content)).toEqual(['u!*(),2', 'u.~-3'])
|
||||
expect(extractMentionedUsers(content)).toEqual(['u!*(),2', 'u.~-3'])
|
||||
})
|
||||
})
|
||||
|
||||
@ -45,13 +45,13 @@ describe('extractIds', () => {
|
||||
it('`href` contains no user id', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/profile" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractIds(content)).toEqual([])
|
||||
expect(extractMentionedUsers(content)).toEqual([])
|
||||
})
|
||||
|
||||
it('`href` is empty or invalid', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="not-a-url" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractIds(content)).toEqual([])
|
||||
expect(extractMentionedUsers(content)).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,6 +1,5 @@
|
||||
import CONFIG from './../config'
|
||||
import activityPub from './activityPubMiddleware'
|
||||
import password from './passwordMiddleware'
|
||||
import softDelete from './softDeleteMiddleware'
|
||||
import sluggify from './sluggifyMiddleware'
|
||||
import excerpt from './excerptMiddleware'
|
||||
@ -10,35 +9,36 @@ import permissions from './permissionsMiddleware'
|
||||
import user from './userMiddleware'
|
||||
import includedFields from './includedFieldsMiddleware'
|
||||
import orderBy from './orderByMiddleware'
|
||||
import validation from './validation'
|
||||
import notifications from './notifications'
|
||||
import validation from './validation/validationMiddleware'
|
||||
import handleContentData from './handleHtmlContent/handleContentData'
|
||||
import email from './email/emailMiddleware'
|
||||
|
||||
export default schema => {
|
||||
const middlewares = {
|
||||
permissions: permissions,
|
||||
activityPub: activityPub,
|
||||
password: password,
|
||||
dateTime: dateTime,
|
||||
validation: validation,
|
||||
sluggify: sluggify,
|
||||
excerpt: excerpt,
|
||||
notifications: notifications,
|
||||
handleContentData: handleContentData,
|
||||
xss: xss,
|
||||
softDelete: softDelete,
|
||||
user: user,
|
||||
includedFields: includedFields,
|
||||
orderBy: orderBy,
|
||||
email: email({ isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT }),
|
||||
}
|
||||
|
||||
let order = [
|
||||
'permissions',
|
||||
'activityPub',
|
||||
'password',
|
||||
// 'activityPub', disabled temporarily
|
||||
'dateTime',
|
||||
'validation',
|
||||
'sluggify',
|
||||
'excerpt',
|
||||
'notifications',
|
||||
'email',
|
||||
'handleContentData',
|
||||
'xss',
|
||||
'softDelete',
|
||||
'user',
|
||||
|
||||
@ -87,6 +87,9 @@ const createOrUpdateLocations = async (userId, locationName, driver) => {
|
||||
}
|
||||
|
||||
const session = driver.session()
|
||||
if (data.place_type.length > 1) {
|
||||
data.id = 'region.' + data.id.split('.')[1]
|
||||
}
|
||||
await createLocation(session, data)
|
||||
|
||||
let parent = data
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
import extractIds from './extractIds'
|
||||
|
||||
const notify = async (resolve, root, args, context, resolveInfo) => {
|
||||
// extract user ids before xss-middleware removes link classes
|
||||
const ids = extractIds(args.content)
|
||||
|
||||
const post = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
const session = context.driver.session()
|
||||
const { id: postId } = post
|
||||
const createdAt = new Date().toISOString()
|
||||
const cypher = `
|
||||
match(u:User) where u.id in $ids
|
||||
match(p:Post) where p.id = $postId
|
||||
create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt})
|
||||
merge (n)-[:NOTIFIED]->(u)
|
||||
merge (p)-[:NOTIFIED]->(n)
|
||||
`
|
||||
await session.run(cypher, { ids, createdAt, postId })
|
||||
session.close()
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: notify,
|
||||
UpdatePost: notify,
|
||||
},
|
||||
}
|
||||
@ -1,125 +0,0 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { host, login } from '../../jest/helpers'
|
||||
import Factory from '../../seed/factories'
|
||||
|
||||
const factory = Factory()
|
||||
let client
|
||||
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
id: 'you',
|
||||
name: 'Al Capone',
|
||||
slug: 'al-capone',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('currentUser { notifications }', () => {
|
||||
const query = `query($read: Boolean) {
|
||||
currentUser {
|
||||
notifications(read: $read, orderBy: createdAt_desc) {
|
||||
read
|
||||
post {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
describe('authenticated', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
describe('given another user', () => {
|
||||
let authorClient
|
||||
let authorParams
|
||||
let authorHeaders
|
||||
|
||||
beforeEach(async () => {
|
||||
authorParams = {
|
||||
email: 'author@example.org',
|
||||
password: '1234',
|
||||
id: 'author',
|
||||
}
|
||||
await factory.create('User', authorParams)
|
||||
authorHeaders = await login(authorParams)
|
||||
})
|
||||
|
||||
describe('who mentions me in a post', () => {
|
||||
let post
|
||||
const title = 'Mentioning Al Capone'
|
||||
const content =
|
||||
'Hey <a class="mention" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
||||
|
||||
beforeEach(async () => {
|
||||
const createPostMutation = `
|
||||
mutation($title: String!, $content: String!) {
|
||||
CreatePost(title: $title, content: $content) {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
authorClient = new GraphQLClient(host, { headers: authorHeaders })
|
||||
const { CreatePost } = await authorClient.request(createPostMutation, { title, content })
|
||||
post = CreatePost
|
||||
})
|
||||
|
||||
it('sends you a notification', async () => {
|
||||
const expectedContent =
|
||||
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
|
||||
const expected = {
|
||||
currentUser: {
|
||||
notifications: [{ read: false, post: { content: expectedContent } }],
|
||||
},
|
||||
}
|
||||
await expect(client.request(query, { read: false })).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
describe('who mentions me again', () => {
|
||||
beforeEach(async () => {
|
||||
const updatedContent = `${post.content} One more mention to <a href="/profile/you" class="mention">@al-capone</a>`
|
||||
// The response `post.content` contains a link but the XSSmiddleware
|
||||
// should have the `mention` CSS class removed. I discovered this
|
||||
// during development and thought: A feature not a bug! This way we
|
||||
// can encode a re-mentioning of users when you edit your post or
|
||||
// comment.
|
||||
const createPostMutation = `
|
||||
mutation($id: ID!, $content: String!) {
|
||||
UpdatePost(id: $id, content: $content) {
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
authorClient = new GraphQLClient(host, { headers: authorHeaders })
|
||||
await authorClient.request(createPostMutation, { id: post.id, content: updatedContent })
|
||||
})
|
||||
|
||||
it('creates exactly one more notification', async () => {
|
||||
const expectedContent =
|
||||
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do? One more mention to <a href="/profile/you" target="_blank">@al-capone</a>'
|
||||
const expected = {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{ read: false, post: { content: expectedContent } },
|
||||
{ read: false, post: { content: expectedContent } },
|
||||
],
|
||||
},
|
||||
}
|
||||
await expect(client.request(query, { read: false })).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,21 +0,0 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import walkRecursive from '../helpers/walkRecursive'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateUser: async (resolve, root, args, context, info) => {
|
||||
args.password = await bcrypt.hashSync(args.password, 10)
|
||||
const result = await resolve(root, args, context, info)
|
||||
result.password = '*****'
|
||||
return result
|
||||
},
|
||||
},
|
||||
Query: async (resolve, root, args, context, info) => {
|
||||
let result = await resolve(root, args, context, info)
|
||||
result = walkRecursive(result, ['password', 'privateKey'], () => {
|
||||
// replace password with asterisk
|
||||
return '*****'
|
||||
})
|
||||
return result
|
||||
},
|
||||
}
|
||||
@ -1,10 +1,12 @@
|
||||
import { rule, shield, deny, allow, or } from 'graphql-shield'
|
||||
import { rule, shield, deny, allow, and, or, not } from 'graphql-shield'
|
||||
|
||||
/*
|
||||
* TODO: implement
|
||||
* See: https://github.com/Human-Connection/Nitro-Backend/pull/40#pullrequestreview-180898363
|
||||
*/
|
||||
const isAuthenticated = rule()(async (parent, args, ctx, info) => {
|
||||
const isAuthenticated = rule({
|
||||
cache: 'contextual',
|
||||
})(async (_parent, _args, ctx, _info) => {
|
||||
return ctx.user !== null
|
||||
})
|
||||
|
||||
@ -68,6 +70,29 @@ const onlyEnabledContent = rule({
|
||||
return !(disabled || deleted)
|
||||
})
|
||||
|
||||
const invitationLimitReached = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (parent, args, { user, driver }) => {
|
||||
const session = driver.session()
|
||||
try {
|
||||
const result = await session.run(
|
||||
`
|
||||
MATCH (user:User {id:$id})-[:GENERATED]->(i:InvitationCode)
|
||||
RETURN COUNT(i) >= 3 as limitReached
|
||||
`,
|
||||
{ id: user.id },
|
||||
)
|
||||
const [limitReached] = result.records.map(record => {
|
||||
return record.get('limitReached')
|
||||
})
|
||||
return limitReached
|
||||
} catch (e) {
|
||||
throw e
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAuthor = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (parent, args, { user, driver }) => {
|
||||
@ -99,37 +124,43 @@ const isDeletingOwnAccount = rule({
|
||||
return context.user.id === args.id
|
||||
})
|
||||
|
||||
const noEmailFilter = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_, args) => {
|
||||
return !('email' in args)
|
||||
})
|
||||
|
||||
// Permissions
|
||||
const permissions = shield(
|
||||
{
|
||||
Query: {
|
||||
'*': deny,
|
||||
findPosts: allow,
|
||||
Category: isAdmin,
|
||||
Tag: isAdmin,
|
||||
Category: allow,
|
||||
Tag: allow,
|
||||
Report: isModerator,
|
||||
Notification: isAdmin,
|
||||
statistics: allow,
|
||||
currentUser: allow,
|
||||
Post: or(onlyEnabledContent, isModerator),
|
||||
Comment: allow,
|
||||
User: allow,
|
||||
User: or(noEmailFilter, isAdmin),
|
||||
isLoggedIn: allow,
|
||||
Badge: allow,
|
||||
},
|
||||
Mutation: {
|
||||
'*': deny,
|
||||
login: allow,
|
||||
SignupByInvitation: allow,
|
||||
Signup: isAdmin,
|
||||
SignupVerification: allow,
|
||||
CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)),
|
||||
UpdateNotification: belongsToMe,
|
||||
CreateUser: isAdmin,
|
||||
UpdateUser: onlyYourself,
|
||||
CreatePost: isAuthenticated,
|
||||
UpdatePost: isAuthor,
|
||||
DeletePost: isAuthor,
|
||||
report: isAuthenticated,
|
||||
CreateBadge: isAdmin,
|
||||
UpdateBadge: isAdmin,
|
||||
DeleteBadge: isAdmin,
|
||||
AddUserBadges: isAdmin,
|
||||
CreateSocialMedia: isAuthenticated,
|
||||
DeleteSocialMedia: isAuthenticated,
|
||||
// AddBadgeRewarded: isAdmin,
|
||||
@ -153,8 +184,6 @@ const permissions = shield(
|
||||
},
|
||||
User: {
|
||||
email: isMyOwn,
|
||||
password: isMyOwn,
|
||||
privateKey: isMyOwn,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -13,12 +13,16 @@ const isUniqueFor = (context, type) => {
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
SignupVerification: async (resolve, root, args, context, info) => {
|
||||
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreateUser: async (resolve, root, args, context, info) => {
|
||||
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
|
||||
UpdatePost: async (resolve, root, args, context, info) => {
|
||||
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreateOrganization: async (resolve, root, args, context, info) => {
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import Factory from '../seed/factories'
|
||||
import { host, login } from '../jest/helpers'
|
||||
import { neode } from '../bootstrap/neo4j'
|
||||
|
||||
let authenticatedClient
|
||||
let headers
|
||||
const factory = Factory()
|
||||
const instance = neode()
|
||||
|
||||
beforeEach(async () => {
|
||||
const adminParams = { role: 'admin', email: 'admin@example.org', password: '1234' }
|
||||
@ -76,33 +78,41 @@ describe('slugify', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('CreateUser', () => {
|
||||
const action = async (mutation, params) => {
|
||||
return authenticatedClient.request(`mutation {
|
||||
${mutation}(password: "yo", email: "123@123.de", ${params}) { slug }
|
||||
}`)
|
||||
describe('SignupVerification', () => {
|
||||
const mutation = `mutation($password: String!, $email: String!, $name: String!, $slug: String, $nonce: String!) {
|
||||
SignupVerification(email: $email, password: $password, name: $name, slug: $slug, nonce: $nonce) { slug }
|
||||
}
|
||||
`
|
||||
|
||||
const action = async variables => {
|
||||
// required for SignupVerification
|
||||
await instance.create('EmailAddress', { email: '123@example.org', nonce: '123456' })
|
||||
|
||||
const defaultVariables = { nonce: '123456', password: 'yo', email: '123@example.org' }
|
||||
return authenticatedClient.request(mutation, { ...defaultVariables, ...variables })
|
||||
}
|
||||
|
||||
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({ name: 'I am a user' })).resolves.toEqual({
|
||||
SignupVerification: { slug: 'i-am-a-user' },
|
||||
})
|
||||
})
|
||||
|
||||
describe('if slug exists', () => {
|
||||
beforeEach(async () => {
|
||||
await action('CreateUser', 'name: "Pre-existing user", slug: "pre-existing-user"')
|
||||
await factory.create('User', { 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({ name: 'pre-existing-user' })).resolves.toEqual({
|
||||
SignupVerification: { slug: 'pre-existing-user-1' },
|
||||
})
|
||||
})
|
||||
|
||||
describe('but if the client specifies a slug', () => {
|
||||
it('rejects CreateUser', async () => {
|
||||
it('rejects SignupVerification', async () => {
|
||||
await expect(
|
||||
action('CreateUser', 'name: "Pre-existing user", slug: "pre-existing-user"'),
|
||||
action({ name: 'Pre-existing user', slug: 'pre-existing-user' }),
|
||||
).rejects.toThrow('already exists')
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,7 +2,7 @@ import createOrUpdateLocations from './nodes/locations'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateUser: async (resolve, root, args, context, info) => {
|
||||
SignupVerification: async (resolve, root, args, context, info) => {
|
||||
const result = await resolve(root, args, context, info)
|
||||
await createOrUpdateLocations(args.id, args.locationName, context.driver)
|
||||
return result
|
||||
|
||||
@ -1,16 +1,5 @@
|
||||
import { UserInputError } from 'apollo-server'
|
||||
|
||||
const USERNAME_MIN_LENGTH = 3
|
||||
|
||||
const validateUsername = async (resolve, root, args, context, info) => {
|
||||
if (!('name' in args) || (args.name && args.name.length >= USERNAME_MIN_LENGTH)) {
|
||||
/* eslint-disable-next-line no-return-await */
|
||||
return await resolve(root, args, context, info)
|
||||
} else {
|
||||
throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} characters long!`)
|
||||
}
|
||||
}
|
||||
|
||||
const validateUrl = async (resolve, root, args, context, info) => {
|
||||
const { url } = args
|
||||
const isValid = url.match(/^(?:https?:\/\/)(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g)
|
||||
@ -34,8 +23,6 @@ const validateUpdateComment = async (resolve, root, args, context, info) => {
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateUser: validateUsername,
|
||||
UpdateUser: validateUsername,
|
||||
CreateSocialMedia: validateUrl,
|
||||
UpdateComment: validateUpdateComment,
|
||||
},
|
||||
|
||||
53
backend/src/middleware/validation/validationMiddleware.js
Normal file
53
backend/src/middleware/validation/validationMiddleware.js
Normal file
@ -0,0 +1,53 @@
|
||||
import { UserInputError } from 'apollo-server'
|
||||
import Joi from '@hapi/joi'
|
||||
|
||||
const COMMENT_MIN_LENGTH = 1
|
||||
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
||||
|
||||
const validate = schema => {
|
||||
return async (resolve, root, args, context, info) => {
|
||||
const validation = schema.validate(args)
|
||||
if (validation.error) throw new UserInputError(validation.error)
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
}
|
||||
|
||||
const socialMediaSchema = Joi.object().keys({
|
||||
url: Joi.string()
|
||||
.uri()
|
||||
.required(),
|
||||
})
|
||||
|
||||
const validateCommentCreation = async (resolve, root, args, context, info) => {
|
||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||
const { postId } = args
|
||||
|
||||
if (!args.content || content.length < COMMENT_MIN_LENGTH) {
|
||||
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
|
||||
}
|
||||
const session = context.driver.session()
|
||||
const postQueryRes = await session.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
RETURN post`,
|
||||
{
|
||||
postId,
|
||||
},
|
||||
)
|
||||
const [post] = postQueryRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
|
||||
if (!post) {
|
||||
throw new UserInputError(NO_POST_ERR_MESSAGE)
|
||||
} else {
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateSocialMedia: validate(socialMediaSchema),
|
||||
CreateComment: validateCommentCreation,
|
||||
},
|
||||
}
|
||||
7
backend/src/models/Badge.js
Normal file
7
backend/src/models/Badge.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
id: { type: 'string', primary: true, lowercase: true },
|
||||
status: { type: 'string', valid: ['permanent', 'temporary'] },
|
||||
type: { type: 'string', valid: ['role', 'crowdfunding'] },
|
||||
icon: { type: 'string', required: true },
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
}
|
||||
12
backend/src/models/EmailAddress.js
Normal file
12
backend/src/models/EmailAddress.js
Normal file
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
email: { type: 'string', primary: true, lowercase: true, email: true },
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
verifiedAt: { type: 'string', isoDate: true },
|
||||
nonce: { type: 'string', token: true },
|
||||
belongsTo: {
|
||||
type: 'relationship',
|
||||
relationship: 'BELONGS_TO',
|
||||
target: 'User',
|
||||
direction: 'out',
|
||||
},
|
||||
}
|
||||
16
backend/src/models/InvitationCode.js
Normal file
16
backend/src/models/InvitationCode.js
Normal file
@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
token: { type: 'string', primary: true, token: true },
|
||||
generatedBy: {
|
||||
type: 'relationship',
|
||||
relationship: 'GENERATED',
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
},
|
||||
activated: {
|
||||
type: 'relationship',
|
||||
relationship: 'ACTIVATED',
|
||||
target: 'EmailAddress',
|
||||
direction: 'out',
|
||||
},
|
||||
}
|
||||
60
backend/src/models/User.js
Normal file
60
backend/src/models/User.js
Normal file
@ -0,0 +1,60 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
module.exports = {
|
||||
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
|
||||
actorId: { type: 'string', allow: [null] },
|
||||
name: { type: 'string', min: 3 },
|
||||
email: { type: 'string', lowercase: true, email: true },
|
||||
slug: 'string',
|
||||
encryptedPassword: 'string',
|
||||
avatar: { type: 'string', allow: [null] },
|
||||
coverImg: { type: 'string', allow: [null] },
|
||||
deleted: { type: 'boolean', default: false },
|
||||
disabled: { type: 'boolean', default: false },
|
||||
role: { type: 'string', default: 'user' },
|
||||
publicKey: 'string',
|
||||
privateKey: 'string',
|
||||
wasInvited: 'boolean',
|
||||
wasSeeded: 'boolean',
|
||||
locationName: { type: 'string', allow: [null] },
|
||||
about: { type: 'string', allow: [null] },
|
||||
primaryEmail: {
|
||||
type: 'relationship',
|
||||
relationship: 'PRIMARY_EMAIL',
|
||||
target: 'EmailAddress',
|
||||
direction: 'out',
|
||||
},
|
||||
following: {
|
||||
type: 'relationship',
|
||||
relationship: 'FOLLOWS',
|
||||
target: 'User',
|
||||
direction: 'out',
|
||||
},
|
||||
followedBy: {
|
||||
type: 'relationship',
|
||||
relationship: 'FOLLOWS',
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
},
|
||||
friends: { type: 'relationship', relationship: 'FRIENDS', target: 'User', direction: 'both' },
|
||||
disabledBy: {
|
||||
type: 'relationship',
|
||||
relationship: 'DISABLED',
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
},
|
||||
rewarded: {
|
||||
type: 'relationship',
|
||||
relationship: 'REWARDED',
|
||||
target: 'Badge',
|
||||
direction: 'in',
|
||||
},
|
||||
invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' },
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
isoDate: true,
|
||||
required: true,
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
20
backend/src/models/User.spec.js
Normal file
20
backend/src/models/User.spec.js
Normal file
@ -0,0 +1,20 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { neode } from '../bootstrap/neo4j'
|
||||
|
||||
const factory = Factory()
|
||||
const instance = neode()
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('role', () => {
|
||||
it('defaults to `user`', async () => {
|
||||
const user = await instance.create('User', { name: 'John' })
|
||||
await expect(user.toJson()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
role: 'user',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
8
backend/src/models/index.js
Normal file
8
backend/src/models/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm
|
||||
// module that is not browser-compatible. Node's `fs` module is server-side only
|
||||
export default {
|
||||
Badge: require('./Badge.js'),
|
||||
User: require('./User.js'),
|
||||
InvitationCode: require('./InvitationCode.js'),
|
||||
EmailAddress: require('./EmailAddress.js'),
|
||||
}
|
||||
@ -12,10 +12,26 @@ export default applyScalars(
|
||||
resolvers,
|
||||
config: {
|
||||
query: {
|
||||
exclude: ['Notfication', 'Statistics', 'LoggedInUser'],
|
||||
exclude: [
|
||||
'Badge',
|
||||
'InvitationCode',
|
||||
'EmailAddress',
|
||||
'Notfication',
|
||||
'Statistics',
|
||||
'LoggedInUser',
|
||||
],
|
||||
// add 'User' here as soon as possible
|
||||
},
|
||||
mutation: {
|
||||
exclude: ['Notfication', 'Statistics', 'LoggedInUser'],
|
||||
exclude: [
|
||||
'Badge',
|
||||
'InvitationCode',
|
||||
'EmailAddress',
|
||||
'Notfication',
|
||||
'Statistics',
|
||||
'LoggedInUser',
|
||||
],
|
||||
// add 'User' here as soon as possible
|
||||
},
|
||||
debug: CONFIG.DEBUG,
|
||||
},
|
||||
|
||||
9
backend/src/schema/resolvers/badges.js
Normal file
9
backend/src/schema/resolvers/badges.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
Badge: async (object, args, context, resolveInfo) => {
|
||||
return neo4jgraphql(object, args, context, resolveInfo, false)
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -1,200 +0,0 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import Factory from '../../seed/factories'
|
||||
import { host, login } from '../../jest/helpers'
|
||||
|
||||
const factory = Factory()
|
||||
let client
|
||||
|
||||
describe('badges', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
email: 'user@example.org',
|
||||
role: 'user',
|
||||
password: '1234',
|
||||
})
|
||||
await factory.create('User', {
|
||||
id: 'u2',
|
||||
role: 'moderator',
|
||||
email: 'moderator@example.org',
|
||||
})
|
||||
await factory.create('User', {
|
||||
id: 'u3',
|
||||
role: 'admin',
|
||||
email: 'admin@example.org',
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('CreateBadge', () => {
|
||||
const variables = {
|
||||
id: 'b1',
|
||||
key: 'indiegogo_en_racoon',
|
||||
type: 'crowdfunding',
|
||||
status: 'permanent',
|
||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
||||
}
|
||||
|
||||
const mutation = `
|
||||
mutation(
|
||||
$id: ID
|
||||
$key: String!
|
||||
$type: BadgeType!
|
||||
$status: BadgeStatus!
|
||||
$icon: String!
|
||||
) {
|
||||
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
|
||||
id,
|
||||
key,
|
||||
type,
|
||||
status,
|
||||
icon
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated admin', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'admin@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
it('creates a badge', async () => {
|
||||
const expected = {
|
||||
CreateBadge: {
|
||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
||||
id: 'b1',
|
||||
key: 'indiegogo_en_racoon',
|
||||
status: 'permanent',
|
||||
type: 'crowdfunding',
|
||||
},
|
||||
}
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated moderator', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdateBadge', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
|
||||
await factory.create('Badge', { id: 'b1' })
|
||||
})
|
||||
const variables = {
|
||||
id: 'b1',
|
||||
key: 'whatever',
|
||||
}
|
||||
|
||||
const mutation = `
|
||||
mutation($id: ID!, $key: String!) {
|
||||
UpdateBadge(id: $id, key: $key) {
|
||||
id
|
||||
key
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated moderator', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated admin', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'admin@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
it('updates a badge', async () => {
|
||||
const expected = {
|
||||
UpdateBadge: {
|
||||
id: 'b1',
|
||||
key: 'whatever',
|
||||
},
|
||||
}
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeleteBadge', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
|
||||
await factory.create('Badge', { id: 'b1' })
|
||||
})
|
||||
const variables = {
|
||||
id: 'b1',
|
||||
}
|
||||
|
||||
const mutation = `
|
||||
mutation($id: ID!) {
|
||||
DeleteBadge(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated moderator', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated admin', () => {
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'admin@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
it('deletes a badge', async () => {
|
||||
const expected = {
|
||||
DeleteBadge: {
|
||||
id: 'b1',
|
||||
},
|
||||
}
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,43 +1,15 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import { UserInputError } from 'apollo-server'
|
||||
|
||||
const COMMENT_MIN_LENGTH = 1
|
||||
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateComment: async (object, params, context, resolveInfo) => {
|
||||
const content = params.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||
const { postId } = params
|
||||
// Adding relationship from comment to post by passing in the postId,
|
||||
// but we do not want to create the comment with postId as an attribute
|
||||
// because we use relationships for this. So, we are deleting it from params
|
||||
// before comment creation.
|
||||
delete params.postId
|
||||
|
||||
if (!params.content || content.length < COMMENT_MIN_LENGTH) {
|
||||
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
|
||||
}
|
||||
if (!postId.trim()) {
|
||||
throw new UserInputError(NO_POST_ERR_MESSAGE)
|
||||
}
|
||||
|
||||
const session = context.driver.session()
|
||||
const postQueryRes = await session.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
RETURN post`,
|
||||
{
|
||||
postId,
|
||||
},
|
||||
)
|
||||
const [post] = postQueryRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
|
||||
if (!post) {
|
||||
throw new UserInputError(NO_POST_ERR_MESSAGE)
|
||||
}
|
||||
const commentWithoutRelationships = await neo4jgraphql(
|
||||
object,
|
||||
params,
|
||||
|
||||
@ -9,13 +9,37 @@ let createCommentVariables
|
||||
let createPostVariables
|
||||
let createCommentVariablesSansPostId
|
||||
let createCommentVariablesWithNonExistentPost
|
||||
let asAuthor
|
||||
let userParams
|
||||
let headers
|
||||
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID!, $title: String!, $content: String!) {
|
||||
CreatePost(id: $id, title: $title, content: $content) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const createCommentMutation = gql`
|
||||
mutation($id: ID, $postId: ID!, $content: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
createPostVariables = {
|
||||
id: 'p1',
|
||||
title: 'post to comment on',
|
||||
content: 'please comment on me',
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
asAuthor = await factory.create('User', {
|
||||
userParams = {
|
||||
name: 'TestUser',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
}
|
||||
await factory.create('User', userParams)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
@ -23,28 +47,6 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
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 = {
|
||||
@ -59,12 +61,8 @@ describe('CreateComment', () => {
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
headers = await login(userParams)
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
@ -72,11 +70,6 @@ describe('CreateComment', () => {
|
||||
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)
|
||||
})
|
||||
|
||||
@ -97,7 +90,7 @@ describe('CreateComment', () => {
|
||||
|
||||
const { User } = await client.request(gql`
|
||||
{
|
||||
User(email: "test@example.org") {
|
||||
User(name: "TestUser") {
|
||||
comments {
|
||||
content
|
||||
}
|
||||
@ -192,45 +185,26 @@ describe('CreateComment', () => {
|
||||
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('ManageComments', () => {
|
||||
let manageCommentsUserParams
|
||||
beforeEach(async () => {
|
||||
asAuthor = await factory.create('User', {
|
||||
manageCommentsUserParams = {
|
||||
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',
|
||||
}
|
||||
createCommentVariables = {
|
||||
id: 'c456',
|
||||
postId: 'p1',
|
||||
content: 'Comment to be deleted',
|
||||
})
|
||||
content: "I'm authorised to comment",
|
||||
}
|
||||
await factory.create('User', manageCommentsUserParams)
|
||||
headers = await login(manageCommentsUserParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
await client.request(createPostMutation, createPostVariables)
|
||||
await client.request(createCommentMutation, createCommentVariables)
|
||||
})
|
||||
|
||||
describe('UpdateComment', () => {
|
||||
@ -244,7 +218,7 @@ describe('ManageComments', () => {
|
||||
`
|
||||
|
||||
let updateCommentVariables = {
|
||||
id: 'c1',
|
||||
id: 'c456',
|
||||
content: 'The comment is updated',
|
||||
}
|
||||
|
||||
@ -259,7 +233,6 @@ describe('ManageComments', () => {
|
||||
|
||||
describe('authenticated but not the author', () => {
|
||||
beforeEach(async () => {
|
||||
let headers
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
@ -277,21 +250,10 @@ describe('ManageComments', () => {
|
||||
})
|
||||
|
||||
describe('authenticated as author', () => {
|
||||
beforeEach(async () => {
|
||||
let headers
|
||||
headers = await login({
|
||||
email: 'author@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
})
|
||||
|
||||
it('updates the comment', async () => {
|
||||
const expected = {
|
||||
UpdateComment: {
|
||||
id: 'c1',
|
||||
id: 'c456',
|
||||
content: 'The comment is updated',
|
||||
},
|
||||
}
|
||||
@ -312,7 +274,7 @@ describe('ManageComments', () => {
|
||||
`
|
||||
|
||||
let deleteCommentVariables = {
|
||||
id: 'c1',
|
||||
id: 'c456',
|
||||
}
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
@ -326,7 +288,6 @@ describe('ManageComments', () => {
|
||||
|
||||
describe('authenticated but not the author', () => {
|
||||
beforeEach(async () => {
|
||||
let headers
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
@ -344,21 +305,10 @@ describe('ManageComments', () => {
|
||||
})
|
||||
|
||||
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',
|
||||
id: 'c456',
|
||||
},
|
||||
}
|
||||
await expect(
|
||||
|
||||
@ -12,7 +12,6 @@ const storeUpload = ({ createReadStream, fileLocation }) =>
|
||||
|
||||
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)
|
||||
|
||||
@ -254,7 +254,7 @@ describe('enable', () => {
|
||||
beforeEach(async () => {
|
||||
authenticateClient = setupAuthenticateClient({
|
||||
role: 'moderator',
|
||||
email: 'someUser@example.org',
|
||||
email: 'someuser@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,22 +1,5 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import CONFIG from '../../config'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { resetPasswordMail, wrongAccountMail } from './passwordReset/emailTemplates'
|
||||
|
||||
const transporter = () => {
|
||||
const configs = {
|
||||
host: CONFIG.SMTP_HOST,
|
||||
port: CONFIG.SMTP_PORT,
|
||||
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
|
||||
secure: false, // true for 465, false for other ports
|
||||
}
|
||||
const { SMTP_USERNAME: user, SMTP_PASSWORD: pass } = CONFIG
|
||||
if (user && pass) {
|
||||
configs.auth = { user, pass }
|
||||
}
|
||||
return nodemailer.createTransport(configs)
|
||||
}
|
||||
|
||||
export async function createPasswordReset(options) {
|
||||
const { driver, code, email, issuedAt = new Date() } = options
|
||||
@ -42,27 +25,28 @@ export default {
|
||||
requestPasswordReset: async (_, { email }, { driver }) => {
|
||||
const code = uuid().substring(0, 6)
|
||||
const [user] = await createPasswordReset({ driver, code, email })
|
||||
if (CONFIG.SMTP_HOST && CONFIG.SMTP_PORT) {
|
||||
const name = (user && user.name) || ''
|
||||
const mailTemplate = user ? resetPasswordMail : wrongAccountMail
|
||||
await transporter().sendMail(mailTemplate({ email, code, name }))
|
||||
}
|
||||
return true
|
||||
const name = (user && user.name) || ''
|
||||
return { user, code, name, response: true }
|
||||
},
|
||||
resetPassword: async (_, { email, code, newPassword }, { driver }) => {
|
||||
const session = driver.session()
|
||||
const stillValid = new Date()
|
||||
stillValid.setDate(stillValid.getDate() - 1)
|
||||
const newHashedPassword = await bcrypt.hashSync(newPassword, 10)
|
||||
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
|
||||
const cypher = `
|
||||
MATCH (pr:PasswordReset {code: $code})
|
||||
MATCH (u:User {email: $email})-[:REQUESTED]->(pr)
|
||||
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
|
||||
SET pr.usedAt = datetime()
|
||||
SET u.password = $newHashedPassword
|
||||
SET u.encryptedPassword = $encryptedNewPassword
|
||||
RETURN pr
|
||||
`
|
||||
let transactionRes = await session.run(cypher, { stillValid, email, code, newHashedPassword })
|
||||
let transactionRes = await session.run(cypher, {
|
||||
stillValid,
|
||||
email,
|
||||
code,
|
||||
encryptedNewPassword,
|
||||
})
|
||||
const [reset] = transactionRes.records.map(record => record.get('pr'))
|
||||
const result = !!(reset && reset.properties.usedAt)
|
||||
session.close()
|
||||
|
||||
@ -1,30 +1,74 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import uuid from 'uuid/v4'
|
||||
import fileUpload from './fileUpload'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
UpdatePost: async (object, params, context, resolveInfo) => {
|
||||
const { categoryIds } = params
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
const session = context.driver.session()
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
|
||||
DELETE previousRelations
|
||||
RETURN post, category
|
||||
`
|
||||
|
||||
await session.run(cypherDeletePreviousRelations, { params })
|
||||
|
||||
let updatePostCypher = `MATCH (post:Post {id: $params.id})
|
||||
SET post = $params
|
||||
`
|
||||
if (categoryIds && categoryIds.length) {
|
||||
updatePostCypher += `WITH post
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (post)-[:CATEGORIZED]->(category)
|
||||
`
|
||||
}
|
||||
updatePostCypher += `RETURN post`
|
||||
const updatePostVariables = { categoryIds, params }
|
||||
|
||||
const transactionRes = await session.run(updatePostCypher, updatePostVariables)
|
||||
const [post] = transactionRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
|
||||
session.close()
|
||||
|
||||
return post.properties
|
||||
},
|
||||
|
||||
CreatePost: async (object, params, context, resolveInfo) => {
|
||||
const { categoryIds } = params
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
const result = await neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
params.id = params.id || uuid()
|
||||
let createPostCypher = `CREATE (post:Post {params})
|
||||
WITH post
|
||||
MATCH (author:User {id: $userId})
|
||||
MERGE (post)<-[:WROTE]-(author)
|
||||
`
|
||||
if (categoryIds) {
|
||||
createPostCypher += `WITH post
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (post)-[:CATEGORIZED]->(category)
|
||||
`
|
||||
}
|
||||
createPostCypher += `RETURN post`
|
||||
const createPostVariables = { userId: context.user.id, categoryIds, params }
|
||||
|
||||
const session = context.driver.session()
|
||||
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,
|
||||
},
|
||||
)
|
||||
const transactionRes = await session.run(createPostCypher, createPostVariables)
|
||||
|
||||
const [post] = transactionRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
|
||||
session.close()
|
||||
|
||||
return result
|
||||
return post.properties
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -4,12 +4,48 @@ import { host, login } from '../../jest/helpers'
|
||||
|
||||
const factory = Factory()
|
||||
let client
|
||||
let userParams
|
||||
let authorParams
|
||||
|
||||
const postTitle = 'I am a title'
|
||||
const postContent = 'Some content'
|
||||
const oldTitle = 'Old title'
|
||||
const oldContent = 'Old content'
|
||||
const newTitle = 'New title'
|
||||
const newContent = 'New content'
|
||||
const createPostVariables = { title: postTitle, content: postContent }
|
||||
const createPostWithCategoriesMutation = `
|
||||
mutation($title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const creatPostWithCategoriesVariables = {
|
||||
title: postTitle,
|
||||
content: postContent,
|
||||
categoryIds: ['cat9', 'cat4', 'cat15'],
|
||||
}
|
||||
const postQueryWithCategories = `
|
||||
query($id: ID) {
|
||||
Post(id: $id) {
|
||||
categories {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
userParams = {
|
||||
name: 'TestUser',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
}
|
||||
authorParams = {
|
||||
email: 'author@example.org',
|
||||
password: '1234',
|
||||
}
|
||||
await factory.create('User', userParams)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
@ -18,8 +54,8 @@ afterEach(async () => {
|
||||
|
||||
describe('CreatePost', () => {
|
||||
const mutation = `
|
||||
mutation {
|
||||
CreatePost(title: "I am a title", content: "Some content") {
|
||||
mutation($title: String!, $content: String!) {
|
||||
CreatePost(title: $title, content: $content) {
|
||||
title
|
||||
content
|
||||
slug
|
||||
@ -32,32 +68,32 @@ describe('CreatePost', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(mutation)).rejects.toThrow('Not Authorised')
|
||||
await expect(client.request(mutation, createPostVariables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
headers = await login(userParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('creates a post', async () => {
|
||||
const expected = {
|
||||
CreatePost: {
|
||||
title: 'I am a title',
|
||||
content: 'Some content',
|
||||
title: postTitle,
|
||||
content: postContent,
|
||||
},
|
||||
}
|
||||
await expect(client.request(mutation)).resolves.toMatchObject(expected)
|
||||
await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
|
||||
})
|
||||
|
||||
it('assigns the authenticated user as author', async () => {
|
||||
await client.request(mutation)
|
||||
await client.request(mutation, createPostVariables)
|
||||
const { User } = await client.request(
|
||||
`{
|
||||
User(email:"test@example.org") {
|
||||
User(name: "TestUser") {
|
||||
contributions {
|
||||
title
|
||||
}
|
||||
@ -65,94 +101,185 @@ describe('CreatePost', () => {
|
||||
}`,
|
||||
{ headers },
|
||||
)
|
||||
expect(User).toEqual([{ contributions: [{ title: 'I am a title' }] }])
|
||||
expect(User).toEqual([{ contributions: [{ title: postTitle }] }])
|
||||
})
|
||||
|
||||
describe('disabled and deleted', () => {
|
||||
it('initially false', async () => {
|
||||
const expected = { CreatePost: { disabled: false, deleted: false } }
|
||||
await expect(client.request(mutation)).resolves.toMatchObject(expected)
|
||||
await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('language', () => {
|
||||
it('allows a user to set the language of the post', async () => {
|
||||
const createPostWithLanguageMutation = `
|
||||
mutation {
|
||||
CreatePost(title: "I am a title", content: "Some content", language: "en") {
|
||||
mutation($title: String!, $content: String!, $language: String) {
|
||||
CreatePost(title: $title, content: $content, language: $language) {
|
||||
language
|
||||
}
|
||||
}
|
||||
`
|
||||
const createPostWithLanguageVariables = {
|
||||
title: postTitle,
|
||||
content: postContent,
|
||||
language: 'en',
|
||||
}
|
||||
const expected = { CreatePost: { language: 'en' } }
|
||||
await expect(client.request(createPostWithLanguageMutation)).resolves.toEqual(
|
||||
expect.objectContaining(expected),
|
||||
await expect(
|
||||
client.request(createPostWithLanguageMutation, createPostWithLanguageVariables),
|
||||
).resolves.toEqual(expect.objectContaining(expected))
|
||||
})
|
||||
})
|
||||
|
||||
describe('categories', () => {
|
||||
it('allows a user to set the categories of the post', async () => {
|
||||
await Promise.all([
|
||||
factory.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
}),
|
||||
factory.create('Category', {
|
||||
id: 'cat4',
|
||||
name: 'Environment & Nature',
|
||||
icon: 'tree',
|
||||
}),
|
||||
factory.create('Category', {
|
||||
id: 'cat15',
|
||||
name: 'Consumption & Sustainability',
|
||||
icon: 'shopping-cart',
|
||||
}),
|
||||
])
|
||||
const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]
|
||||
const postWithCategories = await client.request(
|
||||
createPostWithCategoriesMutation,
|
||||
creatPostWithCategoriesVariables,
|
||||
)
|
||||
const postQueryWithCategoriesVariables = {
|
||||
id: postWithCategories.CreatePost.id,
|
||||
}
|
||||
await expect(
|
||||
client.request(postQueryWithCategories, postQueryWithCategoriesVariables),
|
||||
).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdatePost', () => {
|
||||
const mutation = `
|
||||
mutation($id: ID!, $content: String) {
|
||||
UpdatePost(id: $id, content: $content) {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let variables = {
|
||||
id: 'p1',
|
||||
content: 'New content',
|
||||
}
|
||||
|
||||
let updatePostMutation
|
||||
let updatePostVariables
|
||||
beforeEach(async () => {
|
||||
const asAuthor = Factory()
|
||||
await asAuthor.create('User', {
|
||||
email: 'author@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await asAuthor.authenticateAs({
|
||||
email: 'author@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await asAuthor.create('User', authorParams)
|
||||
await asAuthor.authenticateAs(authorParams)
|
||||
await asAuthor.create('Post', {
|
||||
id: 'p1',
|
||||
content: 'Old content',
|
||||
title: oldTitle,
|
||||
content: oldContent,
|
||||
})
|
||||
updatePostMutation = `
|
||||
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
updatePostVariables = {
|
||||
id: 'p1',
|
||||
title: newTitle,
|
||||
content: newContent,
|
||||
categoryIds: null,
|
||||
}
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow(
|
||||
'Not Authorised',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated but not the author', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
headers = await login(userParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow(
|
||||
'Not Authorised',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated as author', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'author@example.org', password: '1234' })
|
||||
headers = await login(authorParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('updates a post', async () => {
|
||||
const expected = { UpdatePost: { id: 'p1', content: 'New content' } }
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
const expected = { UpdatePost: { id: 'p1', content: newContent } }
|
||||
await expect(client.request(updatePostMutation, updatePostVariables)).resolves.toEqual(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
describe('categories', () => {
|
||||
let postWithCategories
|
||||
beforeEach(async () => {
|
||||
await Promise.all([
|
||||
factory.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
}),
|
||||
factory.create('Category', {
|
||||
id: 'cat4',
|
||||
name: 'Environment & Nature',
|
||||
icon: 'tree',
|
||||
}),
|
||||
factory.create('Category', {
|
||||
id: 'cat15',
|
||||
name: 'Consumption & Sustainability',
|
||||
icon: 'shopping-cart',
|
||||
}),
|
||||
factory.create('Category', {
|
||||
id: 'cat27',
|
||||
name: 'Animal Protection',
|
||||
icon: 'paw',
|
||||
}),
|
||||
])
|
||||
postWithCategories = await client.request(
|
||||
createPostWithCategoriesMutation,
|
||||
creatPostWithCategoriesVariables,
|
||||
)
|
||||
updatePostVariables = {
|
||||
id: postWithCategories.CreatePost.id,
|
||||
title: newTitle,
|
||||
content: newContent,
|
||||
categoryIds: ['cat27'],
|
||||
}
|
||||
})
|
||||
|
||||
it('allows a user to update the categories of a post', async () => {
|
||||
await client.request(updatePostMutation, updatePostVariables)
|
||||
const expected = [{ id: 'cat27' }]
|
||||
const postQueryWithCategoriesVariables = {
|
||||
id: postWithCategories.CreatePost.id,
|
||||
}
|
||||
await expect(
|
||||
client.request(postQueryWithCategories, postQueryWithCategoriesVariables),
|
||||
).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -173,14 +300,8 @@ describe('DeletePost', () => {
|
||||
|
||||
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('User', authorParams)
|
||||
await asAuthor.authenticateAs(authorParams)
|
||||
await asAuthor.create('Post', {
|
||||
id: 'p1',
|
||||
content: 'To be deleted',
|
||||
@ -197,7 +318,7 @@ describe('DeletePost', () => {
|
||||
describe('authenticated but not the author', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
headers = await login(userParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
@ -209,7 +330,7 @@ describe('DeletePost', () => {
|
||||
describe('authenticated as author', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'author@example.org', password: '1234' })
|
||||
headers = await login(authorParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
|
||||
107
backend/src/schema/resolvers/registration.js
Normal file
107
backend/src/schema/resolvers/registration.js
Normal file
@ -0,0 +1,107 @@
|
||||
import { UserInputError } from 'apollo-server'
|
||||
import uuid from 'uuid/v4'
|
||||
import { neode } from '../../bootstrap/neo4j'
|
||||
import fileUpload from './fileUpload'
|
||||
import encryptPassword from '../../helpers/encryptPassword'
|
||||
|
||||
const instance = neode()
|
||||
|
||||
/*
|
||||
* TODO: remove this function as soon type `User` has no `email` property
|
||||
* anymore
|
||||
*/
|
||||
const checkEmailDoesNotExist = async ({ email }) => {
|
||||
email = email.toLowerCase()
|
||||
const users = await instance.all('User', { email })
|
||||
if (users.length > 0) throw new UserInputError('User account with this email already exists.')
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateInvitationCode: async (parent, args, context, resolveInfo) => {
|
||||
args.token = uuid().substring(0, 6)
|
||||
const {
|
||||
user: { id: userId },
|
||||
} = context
|
||||
let response
|
||||
try {
|
||||
const [user, invitationCode] = await Promise.all([
|
||||
instance.find('User', userId),
|
||||
instance.create('InvitationCode', args),
|
||||
])
|
||||
await invitationCode.relateTo(user, 'generatedBy')
|
||||
response = invitationCode.toJson()
|
||||
response.generatedBy = user.toJson()
|
||||
} catch (e) {
|
||||
throw new UserInputError(e)
|
||||
}
|
||||
return response
|
||||
},
|
||||
Signup: async (parent, args, context, resolveInfo) => {
|
||||
const nonce = uuid().substring(0, 6)
|
||||
args.nonce = nonce
|
||||
await checkEmailDoesNotExist({ email: args.email })
|
||||
try {
|
||||
const emailAddress = await instance.create('EmailAddress', args)
|
||||
return { response: emailAddress.toJson(), nonce }
|
||||
} catch (e) {
|
||||
throw new UserInputError(e.message)
|
||||
}
|
||||
},
|
||||
SignupByInvitation: async (parent, args, context, resolveInfo) => {
|
||||
const { token } = args
|
||||
const nonce = uuid().substring(0, 6)
|
||||
args.nonce = nonce
|
||||
await checkEmailDoesNotExist({ email: args.email })
|
||||
try {
|
||||
const result = await instance.cypher(
|
||||
`
|
||||
MATCH (invitationCode:InvitationCode {token:{token}})
|
||||
WHERE NOT (invitationCode)-[:ACTIVATED]->()
|
||||
RETURN invitationCode
|
||||
`,
|
||||
{ token },
|
||||
)
|
||||
const validInvitationCode = instance.hydrateFirst(
|
||||
result,
|
||||
'invitationCode',
|
||||
instance.model('InvitationCode'),
|
||||
)
|
||||
if (!validInvitationCode)
|
||||
throw new UserInputError('Invitation code already used or does not exist.')
|
||||
const emailAddress = await instance.create('EmailAddress', args)
|
||||
await validInvitationCode.relateTo(emailAddress, 'activated')
|
||||
return { response: emailAddress.toJson(), nonce }
|
||||
} catch (e) {
|
||||
throw new UserInputError(e)
|
||||
}
|
||||
},
|
||||
SignupVerification: async (object, args, context, resolveInfo) => {
|
||||
let { nonce, email } = args
|
||||
email = email.toLowerCase()
|
||||
const result = await instance.cypher(
|
||||
`
|
||||
MATCH(email:EmailAddress {nonce: {nonce}, email: {email}})
|
||||
WHERE NOT (email)-[:BELONGS_TO]->()
|
||||
RETURN email
|
||||
`,
|
||||
{ nonce, email },
|
||||
)
|
||||
const emailAddress = await instance.hydrateFirst(result, 'email', instance.model('Email'))
|
||||
if (!emailAddress) throw new UserInputError('Invalid email or nonce')
|
||||
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
|
||||
args = await encryptPassword(args)
|
||||
try {
|
||||
const user = await instance.create('User', args)
|
||||
await Promise.all([
|
||||
user.relateTo(emailAddress, 'primaryEmail'),
|
||||
emailAddress.relateTo(user, 'belongsTo'),
|
||||
emailAddress.update({ verifiedAt: new Date().toISOString() }),
|
||||
])
|
||||
return user.toJson()
|
||||
} catch (e) {
|
||||
throw new UserInputError(e.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
402
backend/src/schema/resolvers/registration.spec.js
Normal file
402
backend/src/schema/resolvers/registration.spec.js
Normal file
@ -0,0 +1,402 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import Factory from '../../seed/factories'
|
||||
import { host, login } from '../../jest/helpers'
|
||||
import { neode } from '../../bootstrap/neo4j'
|
||||
|
||||
let factory
|
||||
let client
|
||||
let variables
|
||||
let action
|
||||
let userParams
|
||||
const instance = neode()
|
||||
|
||||
beforeEach(async () => {
|
||||
variables = {}
|
||||
factory = Factory()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('CreateInvitationCode', () => {
|
||||
const mutation = `mutation { CreateInvitationCode { token } }`
|
||||
|
||||
it('throws Authorization error', async () => {
|
||||
const client = new GraphQLClient(host)
|
||||
await expect(client.request(mutation)).rejects.toThrow('Not Authorised!')
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
userParams = {
|
||||
id: 'i123',
|
||||
name: 'Inviter',
|
||||
email: 'inviter@example.org',
|
||||
password: '1234',
|
||||
}
|
||||
action = async () => {
|
||||
const factory = Factory()
|
||||
await factory.create('User', userParams)
|
||||
const headers = await login(userParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
return client.request(mutation)
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves', async () => {
|
||||
await expect(action()).resolves.toEqual({
|
||||
CreateInvitationCode: { token: expect.any(String) },
|
||||
})
|
||||
})
|
||||
|
||||
it('creates an InvitationCode with a `createdAt` attribute', async () => {
|
||||
await action()
|
||||
const codes = await instance.all('InvitationCode')
|
||||
const invitation = await codes.first().toJson()
|
||||
expect(invitation.createdAt).toBeTruthy()
|
||||
expect(Date.parse(invitation.createdAt)).toEqual(expect.any(Number))
|
||||
})
|
||||
|
||||
it('relates inviting User to InvitationCode', async () => {
|
||||
await action()
|
||||
const result = await instance.cypher(
|
||||
'MATCH(code:InvitationCode)<-[:GENERATED]-(user:User) RETURN user',
|
||||
)
|
||||
const inviter = instance.hydrateFirst(result, 'user', instance.model('User'))
|
||||
await expect(inviter.toJson()).resolves.toEqual(expect.objectContaining({ name: 'Inviter' }))
|
||||
})
|
||||
|
||||
describe('who has invited a lot of users already', () => {
|
||||
beforeEach(() => {
|
||||
action = async () => {
|
||||
const factory = Factory()
|
||||
await factory.create('User', userParams)
|
||||
const headers = await login(userParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
await Promise.all(
|
||||
[1, 2, 3].map(() => {
|
||||
return client.request(mutation)
|
||||
}),
|
||||
)
|
||||
return client.request(mutation, variables)
|
||||
}
|
||||
})
|
||||
|
||||
describe('as ordinary `user`', () => {
|
||||
it('throws `Not Authorised` because of maximum number of invitations', async () => {
|
||||
await expect(action()).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
|
||||
it('creates no additional invitation codes', async done => {
|
||||
try {
|
||||
await action()
|
||||
} catch (e) {
|
||||
const invitationCodes = await instance.all('InvitationCode')
|
||||
await expect(invitationCodes.toJson()).resolves.toHaveLength(3)
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('as a strong donator', () => {
|
||||
beforeEach(() => {
|
||||
// What is the setup?
|
||||
})
|
||||
|
||||
it.todo('can invite more people')
|
||||
// it('can invite more people', async () => {
|
||||
// await action()
|
||||
// const invitationQuery = `{ User { createdAt } }`
|
||||
// const { User: users } = await client.request(invitationQuery )
|
||||
// expect(users).toHaveLength(3 + 1 + 1)
|
||||
// })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SignupByInvitation', () => {
|
||||
const mutation = `mutation($email: String!, $token: String!) {
|
||||
SignupByInvitation(email: $email, token: $token) { email }
|
||||
}`
|
||||
|
||||
beforeEach(() => {
|
||||
client = new GraphQLClient(host)
|
||||
action = async () => {
|
||||
return client.request(mutation, variables)
|
||||
}
|
||||
})
|
||||
|
||||
describe('with valid email but invalid InvitationCode', () => {
|
||||
beforeEach(() => {
|
||||
variables.email = 'any-email@example.org'
|
||||
variables.token = 'wut?'
|
||||
})
|
||||
|
||||
it('throws UserInputError', async () => {
|
||||
await expect(action()).rejects.toThrow('Invitation code already used or does not exist.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with valid InvitationCode', () => {
|
||||
beforeEach(async () => {
|
||||
const inviterParams = {
|
||||
name: 'Inviter',
|
||||
email: 'inviter@example.org',
|
||||
password: '1234',
|
||||
}
|
||||
const factory = Factory()
|
||||
await factory.create('User', inviterParams)
|
||||
const headersOfInviter = await login(inviterParams)
|
||||
const anotherClient = new GraphQLClient(host, { headers: headersOfInviter })
|
||||
const invitationMutation = `mutation { CreateInvitationCode { token } }`
|
||||
const {
|
||||
CreateInvitationCode: { token },
|
||||
} = await anotherClient.request(invitationMutation)
|
||||
variables.token = token
|
||||
})
|
||||
|
||||
describe('given an invalid email', () => {
|
||||
beforeEach(() => {
|
||||
variables.email = 'someuser'
|
||||
})
|
||||
|
||||
it('throws `email is not a valid email`', async () => {
|
||||
await expect(action()).rejects.toThrow('"email" must be a valid email')
|
||||
})
|
||||
|
||||
it('creates no EmailAddress node', async done => {
|
||||
try {
|
||||
await action()
|
||||
} catch (e) {
|
||||
const emailAddresses = await instance.all('EmailAddress')
|
||||
expect(emailAddresses).toHaveLength(0)
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a valid email', () => {
|
||||
beforeEach(() => {
|
||||
variables.email = 'someUser@example.org'
|
||||
})
|
||||
|
||||
it('resolves', async () => {
|
||||
await expect(action()).resolves.toEqual({
|
||||
SignupByInvitation: { email: 'someuser@example.org' },
|
||||
})
|
||||
})
|
||||
|
||||
describe('creates a EmailAddress node', () => {
|
||||
it('with a `createdAt` attribute', async () => {
|
||||
await action()
|
||||
const emailAddresses = await instance.all('EmailAddress')
|
||||
const emailAddress = await emailAddresses.first().toJson()
|
||||
expect(emailAddress.createdAt).toBeTruthy()
|
||||
expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number))
|
||||
})
|
||||
|
||||
it('with a cryptographic `nonce`', async () => {
|
||||
await action()
|
||||
const emailAddresses = await instance.all('EmailAddress')
|
||||
const emailAddress = await emailAddresses.first().toJson()
|
||||
expect(emailAddress.nonce).toEqual(expect.any(String))
|
||||
})
|
||||
|
||||
it('connects inviter through invitation code', async () => {
|
||||
await action()
|
||||
const result = await instance.cypher(
|
||||
'MATCH(inviter:User)-[:GENERATED]->(:InvitationCode)-[:ACTIVATED]->(email:EmailAddress {email: {email}}) RETURN inviter',
|
||||
{ email: 'someuser@example.org' },
|
||||
)
|
||||
const inviter = instance.hydrateFirst(result, 'inviter', instance.model('User'))
|
||||
await expect(inviter.toJson()).resolves.toEqual(
|
||||
expect.objectContaining({ name: 'Inviter' }),
|
||||
)
|
||||
})
|
||||
|
||||
describe('using the same InvitationCode twice', () => {
|
||||
it('rejects because codes can be used only once', async done => {
|
||||
await action()
|
||||
try {
|
||||
await action()
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(/Invitation code already used/)
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('if a user account with the given email already exists', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', { email: 'someuser@example.org' })
|
||||
})
|
||||
|
||||
it('throws unique violation error', async () => {
|
||||
await expect(action()).rejects.toThrow('User account with this email already exists.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('if the EmailAddress already exists but without user account', () => {
|
||||
// shall we re-send the registration email?
|
||||
it.todo('decide what to do')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Signup', () => {
|
||||
const mutation = `mutation($email: String!) {
|
||||
Signup(email: $email) { email }
|
||||
}`
|
||||
|
||||
it('throws AuthorizationError', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(
|
||||
client.request(mutation, { email: 'get-me-a-user-account@example.org' }),
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
|
||||
describe('as admin', () => {
|
||||
beforeEach(async () => {
|
||||
userParams = {
|
||||
role: 'admin',
|
||||
email: 'admin@example.org',
|
||||
password: '1234',
|
||||
}
|
||||
variables.email = 'someuser@example.org'
|
||||
const factory = Factory()
|
||||
await factory.create('User', userParams)
|
||||
const headers = await login(userParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
action = async () => {
|
||||
return client.request(mutation, variables)
|
||||
}
|
||||
})
|
||||
|
||||
it('is allowed to signup users by email', async () => {
|
||||
await expect(action()).resolves.toEqual({ Signup: { email: 'someuser@example.org' } })
|
||||
})
|
||||
|
||||
it('creates a Signup with a cryptographic `nonce`', async () => {
|
||||
await action()
|
||||
const emailAddresses = await instance.all('EmailAddress')
|
||||
const emailAddress = await emailAddresses.first().toJson()
|
||||
expect(emailAddress.nonce).toEqual(expect.any(String))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SignupVerification', () => {
|
||||
const mutation = `
|
||||
mutation($name: String!, $password: String!, $email: String!, $nonce: String!) {
|
||||
SignupVerification(name: $name, password: $password, email: $email, nonce: $nonce) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
describe('given valid password and email', () => {
|
||||
let variables = {
|
||||
nonce: '123456',
|
||||
name: 'John Doe',
|
||||
password: '123',
|
||||
email: 'john@example.org',
|
||||
}
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
beforeEach(async () => {
|
||||
client = new GraphQLClient(host)
|
||||
})
|
||||
|
||||
describe('EmailAddress exists, but is already related to a user account', () => {
|
||||
beforeEach(async () => {
|
||||
const { email, nonce } = variables
|
||||
const [emailAddress, user] = await Promise.all([
|
||||
instance.model('EmailAddress').create({ email, nonce }),
|
||||
instance
|
||||
.model('User')
|
||||
.create({ name: 'Somebody', password: '1234', email: 'john@example.org' }),
|
||||
])
|
||||
await emailAddress.relateTo(user, 'belongsTo')
|
||||
})
|
||||
|
||||
describe('sending a valid nonce', () => {
|
||||
beforeEach(() => {
|
||||
variables.nonce = '123456'
|
||||
})
|
||||
|
||||
it('rejects', async () => {
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow(
|
||||
'Invalid email or nonce',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('disconnected EmailAddress exists', () => {
|
||||
beforeEach(async () => {
|
||||
const args = {
|
||||
email: 'john@example.org',
|
||||
nonce: '123456',
|
||||
}
|
||||
await instance.model('EmailAddress').create(args)
|
||||
})
|
||||
|
||||
describe('sending a valid nonce', () => {
|
||||
it('creates a user account', async () => {
|
||||
const expected = {
|
||||
SignupVerification: {
|
||||
id: expect.any(String),
|
||||
},
|
||||
}
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('sets `verifiedAt` attribute of EmailAddress', async () => {
|
||||
await client.request(mutation, variables)
|
||||
const email = await instance.first('EmailAddress', { email: 'john@example.org' })
|
||||
await expect(email.toJson()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
verifiedAt: expect.any(String),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('connects User with EmailAddress', async () => {
|
||||
const cypher = `
|
||||
MATCH(email:EmailAddress)-[:BELONGS_TO]->(u:User {name: {name}})
|
||||
RETURN email
|
||||
`
|
||||
await client.request(mutation, variables)
|
||||
const { records: emails } = await instance.cypher(cypher, { name: 'John Doe' })
|
||||
expect(emails).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('marks the EmailAddress as primary', async () => {
|
||||
const cypher = `
|
||||
MATCH(email:EmailAddress)<-[:PRIMARY_EMAIL]-(u:User {name: {name}})
|
||||
RETURN email
|
||||
`
|
||||
await client.request(mutation, variables)
|
||||
const { records: emails } = await instance.cypher(cypher, { name: 'John Doe' })
|
||||
expect(emails).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sending invalid nonce', () => {
|
||||
beforeEach(() => {
|
||||
variables.nonce = 'wut2'
|
||||
})
|
||||
|
||||
it('rejects', async () => {
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow(
|
||||
'Invalid email or nonce',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -11,12 +11,31 @@ export default {
|
||||
description: description,
|
||||
}
|
||||
|
||||
const reportQueryRes = await session.run(
|
||||
`
|
||||
match (u:User {id:$submitterId}) -[:REPORTED]->(report)-[:REPORTED]-> (resource {id: $resourceId})
|
||||
return labels(resource)[0] as label
|
||||
`,
|
||||
{
|
||||
resourceId: id,
|
||||
submitterId: user.id,
|
||||
},
|
||||
)
|
||||
const [rep] = reportQueryRes.records.map(record => {
|
||||
return {
|
||||
label: record.get('label'),
|
||||
}
|
||||
})
|
||||
|
||||
if (rep) {
|
||||
throw new Error(rep.label)
|
||||
}
|
||||
const res = await session.run(
|
||||
`
|
||||
MATCH (submitter:User {id: $userId})
|
||||
MATCH (resource {id: $resourceId})
|
||||
WHERE resource:User OR resource:Comment OR resource:Post
|
||||
CREATE (report:Report $reportData)
|
||||
MERGE (report:Report {id: {reportData}.id })
|
||||
MERGE (resource)<-[:REPORTED]-(report)
|
||||
MERGE (report)<-[:REPORTED]-(submitter)
|
||||
RETURN report, submitter, resource, labels(resource)[0] as type
|
||||
@ -27,6 +46,7 @@ export default {
|
||||
reportData,
|
||||
},
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
const [dbResponse] = res.records.map(r => {
|
||||
@ -59,6 +79,7 @@ export default {
|
||||
response.user = resource.properties
|
||||
break
|
||||
}
|
||||
|
||||
return response
|
||||
},
|
||||
},
|
||||
|
||||
@ -13,7 +13,9 @@ describe('report', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
returnedObject = '{ description }'
|
||||
variables = { id: 'whatever' }
|
||||
variables = {
|
||||
id: 'whatever',
|
||||
}
|
||||
headers = {}
|
||||
await factory.create('User', {
|
||||
id: 'u1',
|
||||
@ -42,7 +44,9 @@ describe('report', () => {
|
||||
) ${returnedObject}
|
||||
}
|
||||
`
|
||||
client = new GraphQLClient(host, { headers })
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
return client.request(mutation, variables)
|
||||
}
|
||||
|
||||
@ -53,7 +57,10 @@ describe('report', () => {
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
describe('invalid resource id', () => {
|
||||
@ -66,19 +73,25 @@ describe('report', () => {
|
||||
|
||||
describe('valid resource id', () => {
|
||||
beforeEach(async () => {
|
||||
variables = { id: 'u2' }
|
||||
variables = {
|
||||
id: 'u2',
|
||||
}
|
||||
})
|
||||
|
||||
it('creates a report', async () => {
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: { description: 'Violates code of conduct' },
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
it('creates a report', async () => {
|
||||
await expect(action()).resolves.toEqual({
|
||||
type: null,
|
||||
})
|
||||
})
|
||||
*/
|
||||
it('returns the submitter', async () => {
|
||||
returnedObject = '{ submitter { email } }'
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: { submitter: { email: 'test@example.org' } },
|
||||
report: {
|
||||
submitter: {
|
||||
email: 'test@example.org',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -86,50 +99,72 @@ 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',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('reported resource is a post', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.authenticateAs({ email: 'test@example.org', password: '1234' })
|
||||
await factory.authenticateAs({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await factory.create('Post', {
|
||||
id: 'p23',
|
||||
title: 'Matt and Robert having a pair-programming',
|
||||
})
|
||||
variables = { id: 'p23' }
|
||||
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,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* An der Stelle würde ich den p23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
|
||||
At this point I would check the p23 again, but this time there must be an error message. */
|
||||
|
||||
describe('reported resource is a comment', () => {
|
||||
beforeEach(async () => {
|
||||
createPostVariables = {
|
||||
@ -147,34 +182,54 @@ describe('report', () => {
|
||||
id: 'c34',
|
||||
content: 'Robert getting tired.',
|
||||
})
|
||||
variables = { id: 'c34' }
|
||||
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.',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* An der Stelle würde ich den c34 noch mal prüfen, diesmal muss aber eine error meldung kommen.
|
||||
At this point I would check the c34 again, but this time there must be an error message. */
|
||||
|
||||
describe('reported resource is a tag', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('Tag', { id: 't23' })
|
||||
variables = { id: 't23' }
|
||||
await factory.create('Tag', {
|
||||
id: 't23',
|
||||
})
|
||||
variables = {
|
||||
id: 't23',
|
||||
}
|
||||
})
|
||||
|
||||
it('returns null', async () => {
|
||||
await expect(action()).resolves.toEqual({ report: null })
|
||||
await expect(action()).resolves.toEqual({
|
||||
report: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* An der Stelle würde ich den t23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
|
||||
At this point I would check the t23 again, but this time there must be an error message. */
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,47 +1,47 @@
|
||||
import { neode } from '../../bootstrap/neo4j'
|
||||
import { UserInputError } from 'apollo-server'
|
||||
|
||||
const instance = neode()
|
||||
|
||||
const getUserAndBadge = async ({ badgeKey, userId }) => {
|
||||
let user = await instance.first('User', 'id', userId)
|
||||
const badge = await instance.first('Badge', 'id', badgeKey)
|
||||
if (!user) throw new UserInputError("Couldn't find a user with that id")
|
||||
if (!badge) throw new UserInputError("Couldn't find a badge with that id")
|
||||
return { user, badge }
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
reward: async (_object, params, context, _resolveInfo) => {
|
||||
const { fromBadgeId, toUserId } = params
|
||||
const session = context.driver.session()
|
||||
|
||||
let transactionRes = await session.run(
|
||||
`MATCH (badge:Badge {id: $badgeId}), (rewardedUser:User {id: $rewardedUserId})
|
||||
MERGE (badge)-[:REWARDED]->(rewardedUser)
|
||||
RETURN rewardedUser {.id}`,
|
||||
{
|
||||
badgeId: fromBadgeId,
|
||||
rewardedUserId: toUserId,
|
||||
},
|
||||
)
|
||||
|
||||
const [rewardedUser] = transactionRes.records.map(record => {
|
||||
return record.get('rewardedUser')
|
||||
})
|
||||
|
||||
session.close()
|
||||
|
||||
return rewardedUser.id
|
||||
const { user, badge } = await getUserAndBadge(params)
|
||||
await user.relateTo(badge, 'rewarded')
|
||||
return user.toJson()
|
||||
},
|
||||
|
||||
unreward: async (_object, params, context, _resolveInfo) => {
|
||||
const { fromBadgeId, toUserId } = params
|
||||
const { badgeKey, userId } = params
|
||||
const { user } = await getUserAndBadge(params)
|
||||
const session = context.driver.session()
|
||||
|
||||
let transactionRes = await session.run(
|
||||
`MATCH (badge:Badge {id: $badgeId})-[reward:REWARDED]->(rewardedUser:User {id: $rewardedUserId})
|
||||
DELETE reward
|
||||
RETURN rewardedUser {.id}`,
|
||||
{
|
||||
badgeId: fromBadgeId,
|
||||
rewardedUserId: toUserId,
|
||||
},
|
||||
)
|
||||
const [rewardedUser] = transactionRes.records.map(record => {
|
||||
return record.get('rewardedUser')
|
||||
})
|
||||
session.close()
|
||||
|
||||
return rewardedUser.id
|
||||
try {
|
||||
// silly neode cannot remove relationships
|
||||
await session.run(
|
||||
`
|
||||
MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId})
|
||||
DELETE reward
|
||||
RETURN rewardedUser
|
||||
`,
|
||||
{
|
||||
badgeKey,
|
||||
userId,
|
||||
},
|
||||
)
|
||||
} catch (err) {
|
||||
throw err
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return user.toJson()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import Factory from '../../seed/factories'
|
||||
import { host, login } from '../../jest/helpers'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
const factory = Factory()
|
||||
let user
|
||||
let badge
|
||||
|
||||
describe('rewards', () => {
|
||||
const variables = {
|
||||
from: 'indiegogo_en_rhino',
|
||||
to: 'u1',
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
user = await factory.create('User', {
|
||||
id: 'u1',
|
||||
role: 'user',
|
||||
email: 'user@example.org',
|
||||
@ -22,9 +30,8 @@ describe('rewards', () => {
|
||||
role: 'admin',
|
||||
email: 'admin@example.org',
|
||||
})
|
||||
await factory.create('Badge', {
|
||||
id: 'b6',
|
||||
key: 'indiegogo_en_rhino',
|
||||
badge = await factory.create('Badge', {
|
||||
id: 'indiegogo_en_rhino',
|
||||
type: 'crowdfunding',
|
||||
status: 'permanent',
|
||||
icon: '/img/badges/indiegogo_en_rhino.svg',
|
||||
@ -35,21 +42,19 @@ describe('rewards', () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('RewardBadge', () => {
|
||||
const mutation = `
|
||||
mutation(
|
||||
$from: ID!
|
||||
$to: ID!
|
||||
) {
|
||||
reward(fromBadgeId: $from, toUserId: $to)
|
||||
describe('reward', () => {
|
||||
const mutation = gql`
|
||||
mutation($from: ID!, $to: ID!) {
|
||||
reward(badgeKey: $from, userId: $to) {
|
||||
id
|
||||
badges {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
const variables = {
|
||||
from: 'b6',
|
||||
to: 'u1',
|
||||
}
|
||||
let client
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
@ -65,74 +70,94 @@ describe('rewards', () => {
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
describe('badge for id does not exist', () => {
|
||||
it('rejects with a telling error message', async () => {
|
||||
await expect(
|
||||
client.request(mutation, {
|
||||
...variables,
|
||||
from: 'bullshit',
|
||||
}),
|
||||
).rejects.toThrow("Couldn't find a badge with that id")
|
||||
})
|
||||
})
|
||||
|
||||
describe('user for id does not exist', () => {
|
||||
it('rejects with a telling error message', async () => {
|
||||
await expect(
|
||||
client.request(mutation, {
|
||||
...variables,
|
||||
to: 'bullshit',
|
||||
}),
|
||||
).rejects.toThrow("Couldn't find a user with that id")
|
||||
})
|
||||
})
|
||||
|
||||
it('rewards a badge to user', async () => {
|
||||
const variables = {
|
||||
from: 'b6',
|
||||
to: 'u1',
|
||||
}
|
||||
const expected = {
|
||||
reward: 'u1',
|
||||
reward: {
|
||||
id: 'u1',
|
||||
badges: [{ id: 'indiegogo_en_rhino' }],
|
||||
},
|
||||
}
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('rewards a second different badge to same user', async () => {
|
||||
await factory.create('Badge', {
|
||||
id: 'b1',
|
||||
key: 'indiegogo_en_racoon',
|
||||
type: 'crowdfunding',
|
||||
status: 'permanent',
|
||||
id: 'indiegogo_en_racoon',
|
||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
||||
})
|
||||
const variables = {
|
||||
from: 'b1',
|
||||
to: 'u1',
|
||||
}
|
||||
const expected = {
|
||||
reward: 'u1',
|
||||
reward: {
|
||||
id: 'u1',
|
||||
badges: [{ id: 'indiegogo_en_racoon' }, { id: 'indiegogo_en_rhino' }],
|
||||
},
|
||||
}
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
await client.request(mutation, variables)
|
||||
await expect(
|
||||
client.request(mutation, {
|
||||
...variables,
|
||||
from: 'indiegogo_en_racoon',
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('rewards the same badge as well to another user', async () => {
|
||||
const variables1 = {
|
||||
from: 'b6',
|
||||
to: 'u1',
|
||||
}
|
||||
await client.request(mutation, variables1)
|
||||
|
||||
const variables2 = {
|
||||
from: 'b6',
|
||||
to: 'u2',
|
||||
}
|
||||
const expected = {
|
||||
reward: 'u2',
|
||||
reward: {
|
||||
id: 'u2',
|
||||
badges: [{ id: 'indiegogo_en_rhino' }],
|
||||
},
|
||||
}
|
||||
await expect(client.request(mutation, variables2)).resolves.toEqual(expected)
|
||||
await expect(
|
||||
client.request(mutation, {
|
||||
...variables,
|
||||
to: 'u2',
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
it('returns the original reward if a reward is attempted a second time', async () => {
|
||||
const variables = {
|
||||
from: 'b6',
|
||||
to: 'u1',
|
||||
}
|
||||
|
||||
it('creates no duplicate reward relationships', async () => {
|
||||
await client.request(mutation, variables)
|
||||
await client.request(mutation, variables)
|
||||
|
||||
const query = `{
|
||||
User( id: "u1" ) {
|
||||
badgesCount
|
||||
const query = gql`
|
||||
{
|
||||
User(id: "u1") {
|
||||
badgesCount
|
||||
badges {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const expected = { User: [{ badgesCount: 1 }] }
|
||||
const expected = { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }
|
||||
|
||||
await expect(client.request(query)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated moderator', () => {
|
||||
const variables = {
|
||||
from: 'b6',
|
||||
to: 'u1',
|
||||
}
|
||||
let client
|
||||
beforeEach(async () => {
|
||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||
@ -147,27 +172,41 @@ describe('rewards', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('RemoveReward', () => {
|
||||
describe('unreward', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.relate('User', 'Badges', { from: 'b6', to: 'u1' })
|
||||
await user.relateTo(badge, 'rewarded')
|
||||
})
|
||||
const variables = {
|
||||
from: 'b6',
|
||||
to: 'u1',
|
||||
}
|
||||
const expected = {
|
||||
unreward: 'u1',
|
||||
}
|
||||
const expected = { unreward: { id: 'u1', badges: [] } }
|
||||
|
||||
const mutation = `
|
||||
mutation(
|
||||
$from: ID!
|
||||
$to: ID!
|
||||
) {
|
||||
unreward(fromBadgeId: $from, toUserId: $to)
|
||||
const mutation = gql`
|
||||
mutation($from: ID!, $to: ID!) {
|
||||
unreward(badgeKey: $from, userId: $to) {
|
||||
id
|
||||
badges {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('check test setup', () => {
|
||||
it('user has one badge', async () => {
|
||||
const query = gql`
|
||||
{
|
||||
User(id: "u1") {
|
||||
badgesCount
|
||||
badges {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const expected = { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }
|
||||
const client = new GraphQLClient(host)
|
||||
await expect(client.request(query)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
let client
|
||||
|
||||
@ -188,12 +227,9 @@ describe('rewards', () => {
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('fails to remove a not existing badge from user', async () => {
|
||||
it('does not crash when unrewarding multiple times', 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)).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -98,14 +98,19 @@ describe('SocialMedia', () => {
|
||||
const variables = {
|
||||
url: '',
|
||||
}
|
||||
await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL')
|
||||
await expect(client.request(mutationC, variables)).rejects.toThrow(
|
||||
'"url" is not allowed to be empty',
|
||||
)
|
||||
})
|
||||
|
||||
it('validates URLs', async () => {
|
||||
const variables = {
|
||||
url: 'not-a-url',
|
||||
}
|
||||
await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL')
|
||||
|
||||
await expect(client.request(mutationC, variables)).rejects.toThrow(
|
||||
'"url" must be a valid uri',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -5,7 +5,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
isLoggedIn: (parent, args, { driver, user }) => {
|
||||
isLoggedIn: (_, args, { driver, user }) => {
|
||||
return Boolean(user && user.id)
|
||||
},
|
||||
currentUser: async (object, params, ctx, resolveInfo) => {
|
||||
@ -15,40 +15,29 @@ export default {
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
signup: async (parent, { email, password }, { req }) => {
|
||||
// if (data[email]) {
|
||||
// throw new Error('Another User with same email exists.')
|
||||
// }
|
||||
// data[email] = {
|
||||
// password: await bcrypt.hashSync(password, 10),
|
||||
// }
|
||||
|
||||
return true
|
||||
},
|
||||
login: async (parent, { email, password }, { driver, req, user }) => {
|
||||
login: async (_, { email, password }, { driver, req, user }) => {
|
||||
// if (user && user.id) {
|
||||
// throw new Error('Already logged in.')
|
||||
// }
|
||||
const session = driver.session()
|
||||
const result = await session.run(
|
||||
'MATCH (user:User {email: $userEmail}) ' +
|
||||
'RETURN user {.id, .slug, .name, .avatar, .email, .password, .role, .disabled} as user LIMIT 1',
|
||||
'RETURN user {.id, .slug, .name, .avatar, .email, .encryptedPassword, .role, .disabled} as user LIMIT 1',
|
||||
{
|
||||
userEmail: email,
|
||||
},
|
||||
)
|
||||
|
||||
session.close()
|
||||
const [currentUser] = await result.records.map(function(record) {
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
|
||||
if (
|
||||
currentUser &&
|
||||
(await bcrypt.compareSync(password, currentUser.password)) &&
|
||||
(await bcrypt.compareSync(password, currentUser.encryptedPassword)) &&
|
||||
!currentUser.disabled
|
||||
) {
|
||||
delete currentUser.password
|
||||
delete currentUser.encryptedPassword
|
||||
return encode(currentUser)
|
||||
} else if (currentUser && currentUser.disabled) {
|
||||
throw new AuthenticationError('Your account has been disabled.')
|
||||
@ -60,7 +49,7 @@ export default {
|
||||
const session = driver.session()
|
||||
let result = await session.run(
|
||||
`MATCH (user:User {email: $userEmail})
|
||||
RETURN user {.id, .email, .password}`,
|
||||
RETURN user {.id, .email, .encryptedPassword}`,
|
||||
{
|
||||
userEmail: user.email,
|
||||
},
|
||||
@ -70,22 +59,22 @@ export default {
|
||||
return record.get('user')
|
||||
})
|
||||
|
||||
if (!(await bcrypt.compareSync(oldPassword, currentUser.password))) {
|
||||
if (!(await bcrypt.compareSync(oldPassword, currentUser.encryptedPassword))) {
|
||||
throw new AuthenticationError('Old password is not correct')
|
||||
}
|
||||
|
||||
if (await bcrypt.compareSync(newPassword, currentUser.password)) {
|
||||
if (await bcrypt.compareSync(newPassword, currentUser.encryptedPassword)) {
|
||||
throw new AuthenticationError('Old password and new password should be different')
|
||||
} else {
|
||||
const newHashedPassword = await bcrypt.hashSync(newPassword, 10)
|
||||
const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10)
|
||||
session.run(
|
||||
`MATCH (user:User {email: $userEmail})
|
||||
SET user.password = $newHashedPassword
|
||||
SET user.encryptedPassword = $newEncryptedPassword
|
||||
RETURN user
|
||||
`,
|
||||
{
|
||||
userEmail: user.email,
|
||||
newHashedPassword,
|
||||
newEncryptedPassword,
|
||||
},
|
||||
)
|
||||
session.close()
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import gql from 'graphql-tag'
|
||||
import { GraphQLClient, request } from 'graphql-request'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import CONFIG from './../../config'
|
||||
@ -311,121 +310,3 @@ describe('change password', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('do not expose private RSA key', () => {
|
||||
let headers
|
||||
let client
|
||||
let authenticatedClient
|
||||
|
||||
const queryUserPuplicKey = gql`
|
||||
query($queriedUserSlug: String) {
|
||||
User(slug: $queriedUserSlug) {
|
||||
id
|
||||
publicKey
|
||||
}
|
||||
}
|
||||
`
|
||||
const queryUserPrivateKey = gql`
|
||||
query($queriedUserSlug: String) {
|
||||
User(slug: $queriedUserSlug) {
|
||||
id
|
||||
privateKey
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const generateUserWithKeys = async authenticatedClient => {
|
||||
// Generate user with "privateKey" via 'CreateUser' mutation instead of using the factories "factory.create('User', {...})", see above.
|
||||
const variables = {
|
||||
id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
|
||||
password: 'xYz',
|
||||
slug: 'apfel-strudel',
|
||||
name: 'Apfel Strudel',
|
||||
email: 'apfel-strudel@test.org',
|
||||
}
|
||||
await authenticatedClient.request(
|
||||
gql`
|
||||
mutation($id: ID, $password: String!, $slug: String, $name: String, $email: String!) {
|
||||
CreateUser(id: $id, password: $password, slug: $slug, name: $name, email: $email) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const adminParams = {
|
||||
role: 'admin',
|
||||
email: 'admin@example.org',
|
||||
password: '1234',
|
||||
}
|
||||
// create an admin user who has enough permissions to create other users
|
||||
await factory.create('User', adminParams)
|
||||
const headers = await login(adminParams)
|
||||
authenticatedClient = new GraphQLClient(host, { headers })
|
||||
// but also create an unauthenticated client to issue the `User` query
|
||||
client = new GraphQLClient(host)
|
||||
})
|
||||
|
||||
describe('unauthenticated query of "publicKey" (does the RSA key pair get generated at all?)', () => {
|
||||
it('returns publicKey', async () => {
|
||||
await generateUserWithKeys(authenticatedClient)
|
||||
await expect(
|
||||
await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
User: [
|
||||
{
|
||||
id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
|
||||
publicKey: expect.any(String),
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unauthenticated query of "privateKey"', () => {
|
||||
it('throws "Not Authorised!"', async () => {
|
||||
await generateUserWithKeys(authenticatedClient)
|
||||
await expect(
|
||||
client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }),
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
// authenticate
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
describe('authenticated query of "publicKey"', () => {
|
||||
it('returns publicKey', async () => {
|
||||
await generateUserWithKeys(authenticatedClient)
|
||||
await expect(
|
||||
await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
User: [
|
||||
{
|
||||
id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
|
||||
publicKey: expect.any(String),
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated query of "privateKey"', () => {
|
||||
it('throws "Not Authorised!"', async () => {
|
||||
await generateUserWithKeys(authenticatedClient)
|
||||
await expect(
|
||||
client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }),
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,15 +1,84 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import fileUpload from './fileUpload'
|
||||
import { neode } from '../../bootstrap/neo4j'
|
||||
import { UserInputError } from 'apollo-server'
|
||||
|
||||
const instance = neode()
|
||||
|
||||
const _has = (resolvers, { key, connection }, { returnType }) => {
|
||||
return async (parent, params, context, resolveInfo) => {
|
||||
if (typeof parent[key] !== 'undefined') return parent[key]
|
||||
const { id } = parent
|
||||
const statement = `MATCH(u:User {id: {id}})${connection} RETURN related`
|
||||
const result = await instance.cypher(statement, { id })
|
||||
let response = result.records.map(r => r.get('related').properties)
|
||||
if (returnType === 'object') response = response[0] || null
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
const count = obj => {
|
||||
const resolvers = {}
|
||||
for (const [key, connection] of Object.entries(obj)) {
|
||||
resolvers[key] = async (parent, params, context, resolveInfo) => {
|
||||
if (typeof parent[key] !== 'undefined') return parent[key]
|
||||
const { id } = parent
|
||||
const statement = `
|
||||
MATCH(u:User {id: {id}})${connection}
|
||||
WHERE NOT related.deleted = true AND NOT related.disabled = true
|
||||
RETURN COUNT(DISTINCT(related)) as count
|
||||
`
|
||||
const result = await instance.cypher(statement, { id })
|
||||
const [response] = result.records.map(r => r.get('count').toNumber())
|
||||
return response
|
||||
}
|
||||
}
|
||||
return resolvers
|
||||
}
|
||||
|
||||
const undefinedToNull = list => {
|
||||
const resolvers = {}
|
||||
list.forEach(key => {
|
||||
resolvers[key] = async (parent, params, context, resolveInfo) => {
|
||||
return typeof parent[key] === 'undefined' ? null : parent[key]
|
||||
}
|
||||
})
|
||||
return resolvers
|
||||
}
|
||||
|
||||
export const hasMany = obj => {
|
||||
const resolvers = {}
|
||||
for (const [key, connection] of Object.entries(obj)) {
|
||||
resolvers[key] = _has(resolvers, { key, connection }, { returnType: 'iterable' })
|
||||
}
|
||||
return resolvers
|
||||
}
|
||||
|
||||
export const hasOne = obj => {
|
||||
const resolvers = {}
|
||||
for (const [key, connection] of Object.entries(obj)) {
|
||||
resolvers[key] = _has(resolvers, { key, connection }, { returnType: 'object' })
|
||||
}
|
||||
return resolvers
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
UpdateUser: async (object, params, context, resolveInfo) => {
|
||||
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
|
||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
Query: {
|
||||
User: async (object, args, context, resolveInfo) => {
|
||||
return neo4jgraphql(object, args, context, resolveInfo, false)
|
||||
},
|
||||
CreateUser: async (object, params, context, resolveInfo) => {
|
||||
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
|
||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
},
|
||||
Mutation: {
|
||||
UpdateUser: async (object, args, context, resolveInfo) => {
|
||||
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
|
||||
try {
|
||||
let user = await instance.find('User', args.id)
|
||||
if (!user) return null
|
||||
await user.update(args)
|
||||
return user.toJson()
|
||||
} catch (e) {
|
||||
throw new UserInputError(e.message)
|
||||
}
|
||||
},
|
||||
DeleteUser: async (object, params, context, resolveInfo) => {
|
||||
const { resource } = params
|
||||
@ -34,4 +103,43 @@ export default {
|
||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
},
|
||||
},
|
||||
User: {
|
||||
...undefinedToNull([
|
||||
'actorId',
|
||||
'avatar',
|
||||
'coverImg',
|
||||
'deleted',
|
||||
'disabled',
|
||||
'locationName',
|
||||
'about',
|
||||
]),
|
||||
...count({
|
||||
contributionsCount: '-[:WROTE]->(related:Post)',
|
||||
friendsCount: '<-[:FRIENDS]->(related:User)',
|
||||
followingCount: '-[:FOLLOWS]->(related:User)',
|
||||
followedByCount: '<-[:FOLLOWS]-(related:User)',
|
||||
commentsCount: '-[:WROTE]->(r:Comment)',
|
||||
commentedCount: '-[:WROTE]->(:Comment)-[:COMMENTS]->(related:Post)',
|
||||
shoutedCount: '-[:SHOUTED]->(related:Post)',
|
||||
badgesCount: '<-[:REWARDED]-(related:Badge)',
|
||||
}),
|
||||
...hasOne({
|
||||
invitedBy: '<-[:INVITED]-(related:User)',
|
||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||
}),
|
||||
...hasMany({
|
||||
followedBy: '<-[:FOLLOWS]-(related:User)',
|
||||
following: '-[:FOLLOWS]->(related:User)',
|
||||
friends: '-[:FRIENDS]-(related:User)',
|
||||
blacklisted: '-[:BLACKLISTED]->(related:User)',
|
||||
socialMedia: '-[:OWNED]->(related:SocialMedia)',
|
||||
contributions: '-[:WROTE]->(related:Post)',
|
||||
comments: '-[:WROTE]->(related:Comment)',
|
||||
shouted: '-[:SHOUTED]->(related:Post)',
|
||||
organizationsCreated: '-[:CREATED_ORGA]->(related:Organization)',
|
||||
organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)',
|
||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
badges: '<-[:REWARDED]-(related:Badge)',
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@ -11,50 +11,39 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('users', () => {
|
||||
describe('CreateUser', () => {
|
||||
const mutation = `
|
||||
mutation($name: String, $password: String!, $email: String!) {
|
||||
CreateUser(name: $name, password: $password, email: $email) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
describe('given valid password and email', () => {
|
||||
const variables = {
|
||||
name: 'John Doe',
|
||||
password: '123',
|
||||
email: '123@123.de',
|
||||
}
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
beforeEach(async () => {
|
||||
client = new GraphQLClient(host)
|
||||
})
|
||||
|
||||
it('is not allowed to create users', async () => {
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
describe('User', () => {
|
||||
describe('query by email address', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', { name: 'Johnny', email: 'any-email-address@example.org' })
|
||||
})
|
||||
|
||||
describe('authenticated admin', () => {
|
||||
const query = `query($email: String) { User(email: $email) { name } }`
|
||||
const variables = { email: 'any-email-address@example.org' }
|
||||
beforeEach(() => {
|
||||
client = new GraphQLClient(host)
|
||||
})
|
||||
|
||||
it('is forbidden', async () => {
|
||||
await expect(client.request(query, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
|
||||
describe('as admin', () => {
|
||||
beforeEach(async () => {
|
||||
const adminParams = {
|
||||
const userParams = {
|
||||
role: 'admin',
|
||||
email: 'admin@example.org',
|
||||
password: '1234',
|
||||
}
|
||||
await factory.create('User', adminParams)
|
||||
const headers = await login(adminParams)
|
||||
const factory = Factory()
|
||||
await factory.create('User', userParams)
|
||||
const headers = await login(userParams)
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('is allowed to create new users', async () => {
|
||||
const expected = {
|
||||
CreateUser: {
|
||||
id: expect.any(String),
|
||||
},
|
||||
}
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
it('is permitted', async () => {
|
||||
await expect(client.request(query, variables)).resolves.toEqual({
|
||||
User: [{ name: 'Johnny' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -88,7 +77,7 @@ describe('users', () => {
|
||||
describe('as another user', () => {
|
||||
beforeEach(async () => {
|
||||
const someoneElseParams = {
|
||||
email: 'someoneElse@example.org',
|
||||
email: 'someone-else@example.org',
|
||||
password: '1234',
|
||||
name: 'James Doe',
|
||||
}
|
||||
@ -119,12 +108,12 @@ describe('users', () => {
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('with no name', async () => {
|
||||
it('with `null` as name', async () => {
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: null,
|
||||
}
|
||||
const expected = 'Username must be at least 3 characters long!'
|
||||
const expected = '"name" must be a string'
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow(expected)
|
||||
})
|
||||
|
||||
@ -133,7 +122,7 @@ describe('users', () => {
|
||||
id: 'u47',
|
||||
name: ' ',
|
||||
}
|
||||
const expected = 'Username must be at least 3 characters long!'
|
||||
const expected = '"name" length must be at least 3 characters long'
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow(expected)
|
||||
})
|
||||
})
|
||||
@ -143,7 +132,7 @@ describe('users', () => {
|
||||
let deleteUserVariables
|
||||
let asAuthor
|
||||
const deleteUserMutation = gql`
|
||||
mutation($id: ID!, $resource: [String]) {
|
||||
mutation($id: ID!, $resource: [Deletable]) {
|
||||
DeleteUser(id: $id, resource: $resource) {
|
||||
id
|
||||
contributions {
|
||||
@ -158,13 +147,13 @@ describe('users', () => {
|
||||
}
|
||||
`
|
||||
beforeEach(async () => {
|
||||
asAuthor = await factory.create('User', {
|
||||
await factory.create('User', {
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
id: 'u343',
|
||||
})
|
||||
await factory.create('User', {
|
||||
email: 'friendsAccount@example.org',
|
||||
email: 'friends-account@example.org',
|
||||
password: '1234',
|
||||
id: 'u565',
|
||||
})
|
||||
@ -202,6 +191,7 @@ describe('users', () => {
|
||||
describe('attempting to delete my own account', () => {
|
||||
let expectedResponse
|
||||
beforeEach(async () => {
|
||||
asAuthor = Factory()
|
||||
await asAuthor.authenticateAs({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
enum BadgeStatus {
|
||||
permanent
|
||||
temporary
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
enum BadgeType {
|
||||
role
|
||||
crowdfunding
|
||||
}
|
||||
7
backend/src/schema/types/enum/Emotion.gql
Normal file
7
backend/src/schema/types/enum/Emotion.gql
Normal file
@ -0,0 +1,7 @@
|
||||
enum Emotion {
|
||||
surprised
|
||||
cry
|
||||
happy
|
||||
angry
|
||||
funny
|
||||
}
|
||||
@ -1 +1 @@
|
||||
scalar Upload
|
||||
scalar Upload
|
||||
|
||||
@ -17,21 +17,17 @@ type Query {
|
||||
LIMIT $limit
|
||||
"""
|
||||
)
|
||||
CommentByPost(postId: ID!): [Comment]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
# Get a JWT Token for the given Email and password
|
||||
login(email: String!, password: String!): String!
|
||||
signup(email: String!, password: String!): Boolean!
|
||||
changePassword(oldPassword: String!, newPassword: String!): String!
|
||||
requestPasswordReset(email: String!): Boolean!
|
||||
resetPassword(email: String!, code: String!, newPassword: String!): Boolean!
|
||||
report(id: ID!, description: String): Report
|
||||
disable(id: ID!): ID
|
||||
enable(id: ID!): ID
|
||||
reward(fromBadgeId: ID!, toUserId: ID!): ID
|
||||
unreward(fromBadgeId: ID!, toUserId: ID!): ID
|
||||
# Shout the given Type and ID
|
||||
shout(id: ID!, type: ShoutTypeEnum): Boolean!
|
||||
# Unshout the given Type and ID
|
||||
@ -40,7 +36,6 @@ type Mutation {
|
||||
follow(id: ID!, type: FollowTypeEnum): Boolean!
|
||||
# Unfollow the given Type and ID
|
||||
unfollow(id: ID!, type: FollowTypeEnum): Boolean!
|
||||
DeleteUser(id: ID!, resource: [String]): User
|
||||
}
|
||||
|
||||
type Statistics {
|
||||
@ -92,6 +87,11 @@ type Report {
|
||||
user: User @relation(name: "REPORTED", direction: "OUT")
|
||||
}
|
||||
|
||||
enum Deletable {
|
||||
Post
|
||||
Comment
|
||||
}
|
||||
|
||||
enum ShoutTypeEnum {
|
||||
Post
|
||||
Organization
|
||||
|
||||
@ -1,324 +0,0 @@
|
||||
scalar Upload
|
||||
|
||||
type Query {
|
||||
isLoggedIn: Boolean!
|
||||
# Get the currently logged in User based on the given JWT Token
|
||||
currentUser: User
|
||||
# Get the latest Network Statistics
|
||||
statistics: Statistics!
|
||||
findPosts(filter: String!, limit: Int = 10): [Post]! @cypher(
|
||||
statement: """
|
||||
CALL db.index.fulltext.queryNodes('full_text_search', $filter)
|
||||
YIELD node as post, score
|
||||
MATCH (post)<-[:WROTE]-(user:User)
|
||||
WHERE score >= 0.2
|
||||
AND NOT user.deleted = true AND NOT user.disabled = true
|
||||
AND NOT post.deleted = true AND NOT post.disabled = true
|
||||
RETURN post
|
||||
LIMIT $limit
|
||||
"""
|
||||
)
|
||||
CommentByPost(postId: ID!): [Comment]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
# Get a JWT Token for the given Email and password
|
||||
login(email: String!, password: String!): String!
|
||||
signup(email: String!, password: String!): Boolean!
|
||||
changePassword(oldPassword:String!, newPassword: String!): String!
|
||||
report(id: ID!, description: String): Report
|
||||
disable(id: ID!): ID
|
||||
enable(id: ID!): ID
|
||||
reward(fromBadgeId: ID!, toUserId: ID!): ID
|
||||
unreward(fromBadgeId: ID!, toUserId: ID!): ID
|
||||
# Shout the given Type and ID
|
||||
shout(id: ID!, type: ShoutTypeEnum): Boolean!
|
||||
# Unshout the given Type and ID
|
||||
unshout(id: ID!, type: ShoutTypeEnum): Boolean!
|
||||
# Follow the given Type and ID
|
||||
follow(id: ID!, type: FollowTypeEnum): Boolean!
|
||||
# Unfollow the given Type and ID
|
||||
unfollow(id: ID!, type: FollowTypeEnum): Boolean!
|
||||
}
|
||||
|
||||
type Statistics {
|
||||
countUsers: Int!
|
||||
countPosts: Int!
|
||||
countComments: Int!
|
||||
countNotifications: Int!
|
||||
countOrganizations: Int!
|
||||
countProjects: Int!
|
||||
countInvites: Int!
|
||||
countFollows: Int!
|
||||
countShouts: Int!
|
||||
}
|
||||
|
||||
type Notification {
|
||||
id: ID!
|
||||
read: Boolean,
|
||||
user: User @relation(name: "NOTIFIED", direction: "OUT")
|
||||
post: Post @relation(name: "NOTIFIED", direction: "IN")
|
||||
createdAt: String
|
||||
}
|
||||
|
||||
scalar Date
|
||||
scalar Time
|
||||
scalar DateTime
|
||||
|
||||
enum VisibilityEnum {
|
||||
public
|
||||
friends
|
||||
private
|
||||
}
|
||||
|
||||
enum UserGroupEnum {
|
||||
admin
|
||||
moderator
|
||||
user
|
||||
}
|
||||
|
||||
type Location {
|
||||
id: ID!
|
||||
name: String!
|
||||
nameEN: String
|
||||
nameDE: String
|
||||
nameFR: String
|
||||
nameNL: String
|
||||
nameIT: String
|
||||
nameES: String
|
||||
namePT: String
|
||||
namePL: String
|
||||
type: String!
|
||||
lat: Float
|
||||
lng: Float
|
||||
parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
actorId: String
|
||||
name: String
|
||||
email: String!
|
||||
slug: String
|
||||
password: String!
|
||||
avatar: String
|
||||
avatarUpload: Upload
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
||||
role: UserGroupEnum
|
||||
publicKey: String
|
||||
privateKey: String
|
||||
|
||||
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
||||
locationName: String
|
||||
about: String
|
||||
socialMedia: [SocialMedia]! @relation(name: "OWNED", direction: "OUT")
|
||||
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
|
||||
notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
|
||||
|
||||
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
|
||||
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
|
||||
|
||||
following: [User]! @relation(name: "FOLLOWS", direction: "OUT")
|
||||
followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)")
|
||||
|
||||
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
|
||||
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
|
||||
|
||||
# Is the currently logged in user following that user?
|
||||
followedByCurrentUser: Boolean! @cypher(
|
||||
statement: """
|
||||
MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId})
|
||||
RETURN COUNT(u) >= 1
|
||||
"""
|
||||
)
|
||||
|
||||
#contributions: [WrittenPost]!
|
||||
#contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
|
||||
# @cypher(
|
||||
# statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp"
|
||||
# )
|
||||
contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
|
||||
contributionsCount: Int! @cypher(
|
||||
statement: """
|
||||
MATCH (this)-[:WROTE]->(r:Post)
|
||||
WHERE (NOT exists(r.deleted) OR r.deleted = false)
|
||||
AND (NOT exists(r.disabled) OR r.disabled = false)
|
||||
RETURN COUNT(r)
|
||||
"""
|
||||
)
|
||||
|
||||
comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
|
||||
commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
|
||||
|
||||
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
|
||||
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
|
||||
|
||||
organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT")
|
||||
organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT")
|
||||
|
||||
blacklisted: [User]! @relation(name: "BLACKLISTED", direction: "OUT")
|
||||
|
||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
||||
|
||||
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
||||
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
||||
}
|
||||
|
||||
type Post {
|
||||
id: ID!
|
||||
activityId: String
|
||||
objectId: String
|
||||
author: User @relation(name: "WROTE", direction: "IN")
|
||||
title: String!
|
||||
slug: String
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
image: String
|
||||
imageUpload: Upload
|
||||
visibility: VisibilityEnum
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
|
||||
relatedContributions: [Post]! @cypher(
|
||||
statement: """
|
||||
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
||||
RETURN DISTINCT post
|
||||
LIMIT 10
|
||||
"""
|
||||
)
|
||||
|
||||
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
|
||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
||||
|
||||
comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
|
||||
commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
|
||||
|
||||
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
|
||||
shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
|
||||
|
||||
# Has the currently logged in user shouted that post?
|
||||
shoutedByCurrentUser: Boolean! @cypher(
|
||||
statement: """
|
||||
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
|
||||
RETURN COUNT(u) >= 1
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
type Comment {
|
||||
id: ID!
|
||||
activityId: String
|
||||
postId: ID
|
||||
author: User @relation(name: "WROTE", direction: "IN")
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
post: Post @relation(name: "COMMENTS", direction: "OUT")
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
||||
}
|
||||
|
||||
type Report {
|
||||
id: ID!
|
||||
submitter: User @relation(name: "REPORTED", direction: "IN")
|
||||
description: String
|
||||
type: String! @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]")
|
||||
createdAt: String
|
||||
comment: Comment @relation(name: "REPORTED", direction: "OUT")
|
||||
post: Post @relation(name: "REPORTED", direction: "OUT")
|
||||
user: User @relation(name: "REPORTED", direction: "OUT")
|
||||
}
|
||||
|
||||
type Category {
|
||||
id: ID!
|
||||
name: String!
|
||||
slug: String
|
||||
icon: String!
|
||||
posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN")
|
||||
postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)")
|
||||
}
|
||||
|
||||
type Badge {
|
||||
id: ID!
|
||||
key: String!
|
||||
type: BadgeTypeEnum!
|
||||
status: BadgeStatusEnum!
|
||||
icon: String!
|
||||
|
||||
rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
|
||||
}
|
||||
|
||||
enum BadgeTypeEnum {
|
||||
role
|
||||
crowdfunding
|
||||
}
|
||||
enum BadgeStatusEnum {
|
||||
permanent
|
||||
temporary
|
||||
}
|
||||
enum ShoutTypeEnum {
|
||||
Post
|
||||
Organization
|
||||
Project
|
||||
}
|
||||
enum FollowTypeEnum {
|
||||
User
|
||||
Organization
|
||||
Project
|
||||
}
|
||||
|
||||
type Reward {
|
||||
id: ID!
|
||||
user: User @relation(name: "REWARDED", direction: "IN")
|
||||
rewarderId: ID
|
||||
createdAt: String
|
||||
badge: Badge @relation(name: "REWARDED", direction: "OUT")
|
||||
}
|
||||
|
||||
type Organization {
|
||||
id: ID!
|
||||
createdBy: User @relation(name: "CREATED_ORGA", direction: "IN")
|
||||
ownedBy: [User] @relation(name: "OWNING_ORGA", direction: "IN")
|
||||
name: String!
|
||||
slug: String
|
||||
description: String!
|
||||
descriptionExcerpt: String
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
|
||||
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
|
||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
||||
}
|
||||
|
||||
type Tag {
|
||||
id: ID!
|
||||
name: String!
|
||||
taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
|
||||
taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN")
|
||||
taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)")
|
||||
taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)")
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
}
|
||||
|
||||
type SharedInboxEndpoint {
|
||||
id: ID!
|
||||
uri: String
|
||||
}
|
||||
|
||||
type SocialMedia {
|
||||
id: ID!
|
||||
url: String
|
||||
ownedBy: [User]! @relation(name: "OWNED", direction: "IN")
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
type Badge {
|
||||
id: ID!
|
||||
key: String!
|
||||
type: BadgeType!
|
||||
status: BadgeStatus!
|
||||
icon: String!
|
||||
@ -10,4 +9,23 @@ type Badge {
|
||||
updatedAt: String
|
||||
|
||||
rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
|
||||
}
|
||||
}
|
||||
|
||||
enum BadgeStatus {
|
||||
permanent
|
||||
temporary
|
||||
}
|
||||
|
||||
enum BadgeType {
|
||||
role
|
||||
crowdfunding
|
||||
}
|
||||
|
||||
type Query {
|
||||
Badge: [Badge]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
reward(badgeKey: ID!, userId: ID!): User
|
||||
unreward(badgeKey: ID!, userId: ID!): User
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
type Comment {
|
||||
id: ID!
|
||||
activityId: String
|
||||
postId: ID
|
||||
author: User @relation(name: "WROTE", direction: "IN")
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
@ -11,4 +10,24 @@ type Comment {
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
||||
}
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
CreateComment(
|
||||
id: ID
|
||||
postId: ID!
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
createdAt: String
|
||||
): Comment
|
||||
UpdateComment(
|
||||
id: ID!
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
): Comment
|
||||
DeleteComment(id: ID!): Comment
|
||||
}
|
||||
|
||||
10
backend/src/schema/types/type/EMOTED.gql
Normal file
10
backend/src/schema/types/type/EMOTED.gql
Normal file
@ -0,0 +1,10 @@
|
||||
type EMOTED @relation(name: "EMOTED") {
|
||||
from: User
|
||||
to: Post
|
||||
|
||||
emotion: Emotion
|
||||
#createdAt: DateTime
|
||||
#updatedAt: DateTime
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
}
|
||||
23
backend/src/schema/types/type/EmailAddress.gql
Normal file
23
backend/src/schema/types/type/EmailAddress.gql
Normal file
@ -0,0 +1,23 @@
|
||||
type EmailAddress {
|
||||
id: ID!
|
||||
email: String!
|
||||
verifiedAt: String
|
||||
createdAt: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
Signup(email: String!): EmailAddress
|
||||
SignupByInvitation(email: String!, token: String!): EmailAddress
|
||||
SignupVerification(
|
||||
nonce: String!
|
||||
name: String!
|
||||
email: String!
|
||||
password: String!
|
||||
slug: String
|
||||
avatar: String
|
||||
coverImg: String
|
||||
avatarUpload: Upload
|
||||
locationName: String
|
||||
about: String
|
||||
): User
|
||||
}
|
||||
13
backend/src/schema/types/type/InvitationCode.gql
Normal file
13
backend/src/schema/types/type/InvitationCode.gql
Normal file
@ -0,0 +1,13 @@
|
||||
type InvitationCode {
|
||||
id: ID!
|
||||
token: String
|
||||
generatedBy: User @relation(name: "GENERATED", direction: "IN")
|
||||
|
||||
#createdAt: DateTime
|
||||
#usedAt: DateTime
|
||||
createdAt: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
CreateInvitationCode: InvitationCode
|
||||
}
|
||||
@ -48,4 +48,45 @@ type Post {
|
||||
RETURN COUNT(u) >= 1
|
||||
"""
|
||||
)
|
||||
|
||||
emotions: [EMOTED]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
CreatePost(
|
||||
id: ID
|
||||
activityId: String
|
||||
objectId: String
|
||||
title: String!
|
||||
slug: String
|
||||
content: String!
|
||||
image: String
|
||||
imageUpload: Upload
|
||||
visibility: Visibility
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
language: String
|
||||
categoryIds: [ID]
|
||||
contentExcerpt: String
|
||||
): Post
|
||||
UpdatePost(
|
||||
id: ID!
|
||||
activityId: String
|
||||
objectId: String
|
||||
title: String!
|
||||
slug: String
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
image: String
|
||||
imageUpload: Upload
|
||||
visibility: Visibility
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
language: String
|
||||
categoryIds: [ID]
|
||||
): Post
|
||||
}
|
||||
|
||||
@ -3,20 +3,16 @@ type User {
|
||||
actorId: String
|
||||
name: String
|
||||
email: String!
|
||||
slug: String
|
||||
password: String!
|
||||
slug: String!
|
||||
avatar: String
|
||||
coverImg: String
|
||||
avatarUpload: Upload
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
||||
role: UserGroup
|
||||
role: UserGroup!
|
||||
publicKey: String
|
||||
privateKey: String
|
||||
|
||||
wasInvited: Boolean
|
||||
wasSeeded: Boolean
|
||||
invitedBy: User @relation(name: "INVITED", direction: "IN")
|
||||
invited: [User] @relation(name: "INVITED", direction: "OUT")
|
||||
|
||||
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
||||
locationName: String
|
||||
@ -77,4 +73,92 @@ type User {
|
||||
|
||||
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
||||
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
||||
|
||||
emotions: [EMOTED]
|
||||
}
|
||||
|
||||
|
||||
input _UserFilter {
|
||||
AND: [_UserFilter!]
|
||||
OR: [_UserFilter!]
|
||||
id: ID
|
||||
id_not: ID
|
||||
id_in: [ID!]
|
||||
id_not_in: [ID!]
|
||||
id_contains: ID
|
||||
id_not_contains: ID
|
||||
id_starts_with: ID
|
||||
id_not_starts_with: ID
|
||||
id_ends_with: ID
|
||||
id_not_ends_with: ID
|
||||
friends: _UserFilter
|
||||
friends_not: _UserFilter
|
||||
friends_in: [_UserFilter!]
|
||||
friends_not_in: [_UserFilter!]
|
||||
friends_some: _UserFilter
|
||||
friends_none: _UserFilter
|
||||
friends_single: _UserFilter
|
||||
friends_every: _UserFilter
|
||||
following: _UserFilter
|
||||
following_not: _UserFilter
|
||||
following_in: [_UserFilter!]
|
||||
following_not_in: [_UserFilter!]
|
||||
following_some: _UserFilter
|
||||
following_none: _UserFilter
|
||||
following_single: _UserFilter
|
||||
following_every: _UserFilter
|
||||
followedBy: _UserFilter
|
||||
followedBy_not: _UserFilter
|
||||
followedBy_in: [_UserFilter!]
|
||||
followedBy_not_in: [_UserFilter!]
|
||||
followedBy_some: _UserFilter
|
||||
followedBy_none: _UserFilter
|
||||
followedBy_single: _UserFilter
|
||||
followedBy_every: _UserFilter
|
||||
}
|
||||
|
||||
type Query {
|
||||
User(
|
||||
id: ID
|
||||
email: String
|
||||
actorId: String
|
||||
name: String
|
||||
slug: String
|
||||
avatar: String
|
||||
coverImg: String
|
||||
role: UserGroup
|
||||
locationName: String
|
||||
about: String
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
friendsCount: Int
|
||||
followingCount: Int
|
||||
followedByCount: Int
|
||||
followedByCurrentUser: Boolean
|
||||
contributionsCount: Int
|
||||
commentsCount: Int
|
||||
commentedCount: Int
|
||||
shoutedCount: Int
|
||||
badgesCount: Int
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_UserOrdering]
|
||||
filter: _UserFilter
|
||||
): [User]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
UpdateUser (
|
||||
id: ID!
|
||||
name: String
|
||||
email: String
|
||||
slug: String
|
||||
avatar: String
|
||||
coverImg: String
|
||||
avatarUpload: Upload
|
||||
locationName: String
|
||||
about: String
|
||||
): User
|
||||
|
||||
DeleteUser(id: ID!, resource: [Deletable]): User
|
||||
}
|
||||
|
||||
@ -1,28 +1,15 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
export default function(params) {
|
||||
const {
|
||||
id = uuid(),
|
||||
key = '',
|
||||
type = 'crowdfunding',
|
||||
status = 'permanent',
|
||||
icon = '/img/badges/indiegogo_en_panda.svg',
|
||||
} = params
|
||||
|
||||
export default function create() {
|
||||
return {
|
||||
mutation: `
|
||||
mutation(
|
||||
$id: ID
|
||||
$key: String!
|
||||
$type: BadgeType!
|
||||
$status: BadgeStatus!
|
||||
$icon: String!
|
||||
) {
|
||||
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
|
||||
id
|
||||
}
|
||||
factory: async ({ args, neodeInstance }) => {
|
||||
const defaults = {
|
||||
type: 'crowdfunding',
|
||||
status: 'permanent',
|
||||
}
|
||||
`,
|
||||
variables: { id, key, type, status, icon },
|
||||
args = {
|
||||
...defaults,
|
||||
...args,
|
||||
}
|
||||
return neodeInstance.create('Badge', args)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ export default function(params) {
|
||||
|
||||
return {
|
||||
mutation: `
|
||||
mutation($id: ID!, $postId: ID, $content: String!) {
|
||||
mutation($id: ID!, $postId: ID!, $content: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||
id
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { GraphQLClient, request } from 'graphql-request'
|
||||
import { getDriver } from '../../bootstrap/neo4j'
|
||||
import { getDriver, neode } from '../../bootstrap/neo4j'
|
||||
import createBadge from './badges.js'
|
||||
import createUser from './users.js'
|
||||
import createOrganization from './organizations.js'
|
||||
@ -48,7 +48,11 @@ export const cleanDatabase = async (options = {}) => {
|
||||
}
|
||||
|
||||
export default function Factory(options = {}) {
|
||||
const { neo4jDriver = getDriver(), seedServerHost = 'http://127.0.0.1:4001' } = options
|
||||
let {
|
||||
seedServerHost = 'http://127.0.0.1:4001',
|
||||
neo4jDriver = getDriver(),
|
||||
neodeInstance = neode(),
|
||||
} = options
|
||||
|
||||
const graphQLClient = new GraphQLClient(seedServerHost)
|
||||
|
||||
@ -58,19 +62,24 @@ export default function Factory(options = {}) {
|
||||
graphQLClient,
|
||||
factories,
|
||||
lastResponse: null,
|
||||
neodeInstance,
|
||||
async authenticateAs({ email, password }) {
|
||||
const headers = await authenticatedHeaders({ email, password }, seedServerHost)
|
||||
this.lastResponse = headers
|
||||
this.graphQLClient = new GraphQLClient(seedServerHost, { headers })
|
||||
return this
|
||||
},
|
||||
async create(node, properties) {
|
||||
const { mutation, variables } = this.factories[node](properties)
|
||||
this.lastResponse = await this.graphQLClient.request(mutation, variables)
|
||||
async create(node, args = {}) {
|
||||
const { factory, mutation, variables } = this.factories[node](args)
|
||||
if (factory) {
|
||||
this.lastResponse = await factory({ args, neodeInstance })
|
||||
return this.lastResponse
|
||||
} else {
|
||||
this.lastResponse = await this.graphQLClient.request(mutation, variables)
|
||||
}
|
||||
return this
|
||||
},
|
||||
async relate(node, relationship, properties) {
|
||||
const { from, to } = properties
|
||||
async relate(node, relationship, { from, to }) {
|
||||
const mutation = `
|
||||
mutation {
|
||||
Add${node}${relationship}(
|
||||
@ -112,6 +121,11 @@ export default function Factory(options = {}) {
|
||||
this.lastResponse = await this.graphQLClient.request(mutation)
|
||||
return this
|
||||
},
|
||||
async invite({ email }) {
|
||||
const mutation = ` mutation($email: String!) { invite( email: $email) } `
|
||||
this.lastResponse = await this.graphQLClient.request(mutation, { email })
|
||||
return this
|
||||
},
|
||||
async cleanDatabase() {
|
||||
this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver })
|
||||
return this
|
||||
@ -121,6 +135,9 @@ export default function Factory(options = {}) {
|
||||
result.create.bind(result)
|
||||
result.relate.bind(result)
|
||||
result.mutate.bind(result)
|
||||
result.shout.bind(result)
|
||||
result.follow.bind(result)
|
||||
result.invite.bind(result)
|
||||
result.cleanDatabase.bind(result)
|
||||
return result
|
||||
}
|
||||
|
||||
@ -8,10 +8,12 @@ export default function create(params) {
|
||||
mutation($id: ID!, $description: String!) {
|
||||
report(description: $description, id: $id) {
|
||||
id
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id, description },
|
||||
variables: {
|
||||
id,
|
||||
description,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,51 +1,27 @@
|
||||
import faker from 'faker'
|
||||
import uuid from 'uuid/v4'
|
||||
import encryptPassword from '../../helpers/encryptPassword'
|
||||
import slugify from 'slug'
|
||||
|
||||
export default function create(params) {
|
||||
const {
|
||||
id = uuid(),
|
||||
name = faker.name.findName(),
|
||||
slug = '',
|
||||
email = faker.internet.email(),
|
||||
password = '1234',
|
||||
role = 'user',
|
||||
avatar = faker.internet.avatar(),
|
||||
about = faker.lorem.paragraph(),
|
||||
} = params
|
||||
|
||||
export default function create() {
|
||||
return {
|
||||
mutation: `
|
||||
mutation(
|
||||
$id: ID!
|
||||
$name: String
|
||||
$slug: String
|
||||
$password: String!
|
||||
$email: String!
|
||||
$avatar: String
|
||||
$about: String
|
||||
$role: UserGroup
|
||||
) {
|
||||
CreateUser(
|
||||
id: $id
|
||||
name: $name
|
||||
slug: $slug
|
||||
password: $password
|
||||
email: $email
|
||||
avatar: $avatar
|
||||
about: $about
|
||||
role: $role
|
||||
) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
email
|
||||
avatar
|
||||
role
|
||||
deleted
|
||||
disabled
|
||||
}
|
||||
factory: async ({ args, neodeInstance }) => {
|
||||
const defaults = {
|
||||
id: uuid(),
|
||||
name: faker.name.findName(),
|
||||
email: faker.internet.email(),
|
||||
password: '1234',
|
||||
role: 'user',
|
||||
avatar: faker.internet.avatar(),
|
||||
about: faker.lorem.paragraph(),
|
||||
}
|
||||
`,
|
||||
variables: { id, name, slug, password, email, avatar, about, role },
|
||||
defaults.slug = slugify(defaults.name, { lower: true })
|
||||
args = {
|
||||
...defaults,
|
||||
...args,
|
||||
}
|
||||
args = await encryptPassword(args)
|
||||
return neodeInstance.create('User', args)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,52 +5,42 @@ import Factory from './factories'
|
||||
;(async function() {
|
||||
try {
|
||||
const f = Factory()
|
||||
await Promise.all([
|
||||
const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([
|
||||
f.create('Badge', {
|
||||
id: 'b1',
|
||||
key: 'indiegogo_en_racoon',
|
||||
type: 'crowdfunding',
|
||||
status: 'permanent',
|
||||
id: 'indiegogo_en_racoon',
|
||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
||||
}),
|
||||
f.create('Badge', {
|
||||
id: 'b2',
|
||||
key: 'indiegogo_en_rabbit',
|
||||
type: 'crowdfunding',
|
||||
status: 'permanent',
|
||||
id: 'indiegogo_en_rabbit',
|
||||
icon: '/img/badges/indiegogo_en_rabbit.svg',
|
||||
}),
|
||||
f.create('Badge', {
|
||||
id: 'b3',
|
||||
key: 'indiegogo_en_wolf',
|
||||
type: 'crowdfunding',
|
||||
status: 'permanent',
|
||||
id: 'indiegogo_en_wolf',
|
||||
icon: '/img/badges/indiegogo_en_wolf.svg',
|
||||
}),
|
||||
f.create('Badge', {
|
||||
id: 'b4',
|
||||
key: 'indiegogo_en_bear',
|
||||
type: 'crowdfunding',
|
||||
status: 'permanent',
|
||||
id: 'indiegogo_en_bear',
|
||||
icon: '/img/badges/indiegogo_en_bear.svg',
|
||||
}),
|
||||
f.create('Badge', {
|
||||
id: 'b5',
|
||||
key: 'indiegogo_en_turtle',
|
||||
type: 'crowdfunding',
|
||||
status: 'permanent',
|
||||
id: 'indiegogo_en_turtle',
|
||||
icon: '/img/badges/indiegogo_en_turtle.svg',
|
||||
}),
|
||||
f.create('Badge', {
|
||||
id: 'b6',
|
||||
key: 'indiegogo_en_rhino',
|
||||
type: 'crowdfunding',
|
||||
status: 'permanent',
|
||||
id: 'indiegogo_en_rhino',
|
||||
icon: '/img/badges/indiegogo_en_rhino.svg',
|
||||
}),
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
const [
|
||||
peterLustig,
|
||||
bobDerBaumeister,
|
||||
jennyRostock,
|
||||
tick, // eslint-disable-line no-unused-vars
|
||||
trick, // eslint-disable-line no-unused-vars
|
||||
track, // eslint-disable-line no-unused-vars
|
||||
dagobert,
|
||||
] = await Promise.all([
|
||||
f.create('User', {
|
||||
id: 'u1',
|
||||
name: 'Peter Lustig',
|
||||
@ -69,47 +59,130 @@ import Factory from './factories'
|
||||
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: 'u4',
|
||||
name: 'Tick',
|
||||
role: 'user',
|
||||
email: 'tick@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u5',
|
||||
name: 'Trick',
|
||||
role: 'user',
|
||||
email: 'trick@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u6',
|
||||
name: 'Track',
|
||||
role: 'user',
|
||||
email: 'track@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u7',
|
||||
name: 'Dagobert',
|
||||
role: 'user',
|
||||
email: 'dagobert@example.org',
|
||||
}),
|
||||
])
|
||||
|
||||
const [asAdmin, asModerator, asUser, asTick, asTrick, asTrack] = await Promise.all([
|
||||
Factory().authenticateAs({ email: 'admin@example.org', password: '1234' }),
|
||||
Factory().authenticateAs({ email: 'moderator@example.org', password: '1234' }),
|
||||
Factory().authenticateAs({ email: 'user@example.org', password: '1234' }),
|
||||
Factory().authenticateAs({ email: 'tick@example.org', password: '1234' }),
|
||||
Factory().authenticateAs({ email: 'trick@example.org', password: '1234' }),
|
||||
Factory().authenticateAs({ email: 'track@example.org', password: '1234' }),
|
||||
Factory().authenticateAs({
|
||||
email: 'admin@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
Factory().authenticateAs({
|
||||
email: 'moderator@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
Factory().authenticateAs({
|
||||
email: 'user@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
Factory().authenticateAs({
|
||||
email: 'tick@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
Factory().authenticateAs({
|
||||
email: 'trick@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
Factory().authenticateAs({
|
||||
email: 'track@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
f.relate('User', 'Badges', { from: 'b6', to: 'u1' }),
|
||||
f.relate('User', 'Badges', { from: 'b5', to: 'u2' }),
|
||||
f.relate('User', 'Badges', { from: 'b4', to: 'u3' }),
|
||||
f.relate('User', 'Badges', { from: 'b3', to: 'u4' }),
|
||||
f.relate('User', 'Badges', { from: 'b2', to: 'u5' }),
|
||||
f.relate('User', 'Badges', { from: 'b1', to: 'u6' }),
|
||||
f.relate('User', 'Friends', { from: 'u1', to: 'u2' }),
|
||||
f.relate('User', 'Friends', { from: 'u1', to: 'u3' }),
|
||||
f.relate('User', 'Friends', { from: 'u2', to: 'u3' }),
|
||||
f.relate('User', 'Blacklisted', { from: 'u7', to: 'u4' }),
|
||||
f.relate('User', 'Blacklisted', { from: 'u7', to: 'u5' }),
|
||||
f.relate('User', 'Blacklisted', { from: 'u7', to: 'u6' }),
|
||||
peterLustig.relateTo(racoon, 'rewarded'),
|
||||
peterLustig.relateTo(rhino, 'rewarded'),
|
||||
peterLustig.relateTo(wolf, 'rewarded'),
|
||||
bobDerBaumeister.relateTo(racoon, 'rewarded'),
|
||||
bobDerBaumeister.relateTo(turtle, 'rewarded'),
|
||||
jennyRostock.relateTo(bear, 'rewarded'),
|
||||
dagobert.relateTo(rabbit, 'rewarded'),
|
||||
])
|
||||
|
||||
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' }),
|
||||
f.relate('User', 'Friends', {
|
||||
from: 'u1',
|
||||
to: 'u2',
|
||||
}),
|
||||
f.relate('User', 'Friends', {
|
||||
from: 'u1',
|
||||
to: 'u3',
|
||||
}),
|
||||
f.relate('User', 'Friends', {
|
||||
from: 'u2',
|
||||
to: 'u3',
|
||||
}),
|
||||
f.relate('User', 'Blacklisted', {
|
||||
from: 'u7',
|
||||
to: 'u4',
|
||||
}),
|
||||
f.relate('User', 'Blacklisted', {
|
||||
from: 'u7',
|
||||
to: 'u5',
|
||||
}),
|
||||
f.relate('User', 'Blacklisted', {
|
||||
from: 'u7',
|
||||
to: 'u6',
|
||||
}),
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
f.create('Category', { id: 'cat1', name: 'Just For Fun', slug: 'justforfun', icon: 'smile' }),
|
||||
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',
|
||||
@ -203,10 +276,22 @@ import Factory from './factories'
|
||||
])
|
||||
|
||||
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: 'Umwelt',
|
||||
name: 'Umwelt',
|
||||
}),
|
||||
f.create('Tag', {
|
||||
id: 'Naturschutz',
|
||||
name: 'Naturschutz',
|
||||
}),
|
||||
f.create('Tag', {
|
||||
id: 'Demokratie',
|
||||
name: 'Demokratie',
|
||||
}),
|
||||
f.create('Tag', {
|
||||
id: 'Freiheit',
|
||||
name: 'Freiheit',
|
||||
}),
|
||||
])
|
||||
|
||||
const mention1 = 'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
|
||||
@ -214,108 +299,347 @@ import Factory from './factories'
|
||||
'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, here is another notification for you!'
|
||||
|
||||
await Promise.all([
|
||||
asAdmin.create('Post', { id: 'p0', image: faker.image.unsplash.food() }),
|
||||
asModerator.create('Post', { id: 'p1', image: faker.image.unsplash.technology() }),
|
||||
asUser.create('Post', { id: 'p2' }),
|
||||
asTick.create('Post', { id: 'p3' }),
|
||||
asTrick.create('Post', { id: 'p4' }),
|
||||
asTrack.create('Post', { id: 'p5' }),
|
||||
asAdmin.create('Post', { id: 'p6', image: faker.image.unsplash.buildings() }),
|
||||
asModerator.create('Post', { id: 'p7', content: `${mention1} ${faker.lorem.paragraph()}` }),
|
||||
asUser.create('Post', { id: 'p8', image: faker.image.unsplash.nature() }),
|
||||
asTick.create('Post', { id: 'p9' }),
|
||||
asTrick.create('Post', { id: 'p10' }),
|
||||
asTrack.create('Post', { id: 'p11', image: faker.image.unsplash.people() }),
|
||||
asAdmin.create('Post', { id: 'p12', content: `${mention2} ${faker.lorem.paragraph()}` }),
|
||||
asModerator.create('Post', { id: 'p13' }),
|
||||
asUser.create('Post', { id: 'p14', image: faker.image.unsplash.objects() }),
|
||||
asTick.create('Post', { id: 'p15' }),
|
||||
asAdmin.create('Post', {
|
||||
id: 'p0',
|
||||
image: faker.image.unsplash.food(),
|
||||
}),
|
||||
asModerator.create('Post', {
|
||||
id: 'p1',
|
||||
image: faker.image.unsplash.technology(),
|
||||
}),
|
||||
asUser.create('Post', {
|
||||
id: 'p2',
|
||||
}),
|
||||
asTick.create('Post', {
|
||||
id: 'p3',
|
||||
}),
|
||||
asTrick.create('Post', {
|
||||
id: 'p4',
|
||||
}),
|
||||
asTrack.create('Post', {
|
||||
id: 'p5',
|
||||
}),
|
||||
asAdmin.create('Post', {
|
||||
id: 'p6',
|
||||
image: faker.image.unsplash.buildings(),
|
||||
}),
|
||||
asModerator.create('Post', {
|
||||
id: 'p7',
|
||||
content: `${mention1} ${faker.lorem.paragraph()}`,
|
||||
}),
|
||||
asUser.create('Post', {
|
||||
id: 'p8',
|
||||
image: faker.image.unsplash.nature(),
|
||||
}),
|
||||
asTick.create('Post', {
|
||||
id: 'p9',
|
||||
}),
|
||||
asTrick.create('Post', {
|
||||
id: 'p10',
|
||||
}),
|
||||
asTrack.create('Post', {
|
||||
id: 'p11',
|
||||
image: faker.image.unsplash.people(),
|
||||
}),
|
||||
asAdmin.create('Post', {
|
||||
id: 'p12',
|
||||
content: `${mention2} ${faker.lorem.paragraph()}`,
|
||||
}),
|
||||
asModerator.create('Post', {
|
||||
id: 'p13',
|
||||
}),
|
||||
asUser.create('Post', {
|
||||
id: 'p14',
|
||||
image: faker.image.unsplash.objects(),
|
||||
}),
|
||||
asTick.create('Post', {
|
||||
id: 'p15',
|
||||
}),
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
f.relate('Post', 'Categories', { from: 'p0', to: 'cat16' }),
|
||||
f.relate('Post', 'Categories', { from: 'p1', to: 'cat1' }),
|
||||
f.relate('Post', 'Categories', { from: 'p2', to: 'cat2' }),
|
||||
f.relate('Post', 'Categories', { from: 'p3', to: 'cat3' }),
|
||||
f.relate('Post', 'Categories', { from: 'p4', to: 'cat4' }),
|
||||
f.relate('Post', 'Categories', { from: 'p5', to: 'cat5' }),
|
||||
f.relate('Post', 'Categories', { from: 'p6', to: 'cat6' }),
|
||||
f.relate('Post', 'Categories', { from: 'p7', to: 'cat7' }),
|
||||
f.relate('Post', 'Categories', { from: 'p8', to: 'cat8' }),
|
||||
f.relate('Post', 'Categories', { from: 'p9', to: 'cat9' }),
|
||||
f.relate('Post', 'Categories', { from: 'p10', to: 'cat10' }),
|
||||
f.relate('Post', 'Categories', { from: 'p11', to: 'cat11' }),
|
||||
f.relate('Post', 'Categories', { from: 'p12', to: 'cat12' }),
|
||||
f.relate('Post', 'Categories', { from: 'p13', to: 'cat13' }),
|
||||
f.relate('Post', 'Categories', { from: 'p14', to: 'cat14' }),
|
||||
f.relate('Post', 'Categories', { from: 'p15', to: 'cat15' }),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p0',
|
||||
to: 'cat16',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p1',
|
||||
to: 'cat1',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p2',
|
||||
to: 'cat2',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p3',
|
||||
to: 'cat3',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p4',
|
||||
to: 'cat4',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p5',
|
||||
to: 'cat5',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p6',
|
||||
to: 'cat6',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p7',
|
||||
to: 'cat7',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p8',
|
||||
to: 'cat8',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p9',
|
||||
to: 'cat9',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p10',
|
||||
to: 'cat10',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p11',
|
||||
to: 'cat11',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p12',
|
||||
to: 'cat12',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p13',
|
||||
to: 'cat13',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p14',
|
||||
to: 'cat14',
|
||||
}),
|
||||
f.relate('Post', 'Categories', {
|
||||
from: 'p15',
|
||||
to: 'cat15',
|
||||
}),
|
||||
|
||||
f.relate('Post', 'Tags', { from: 'p0', to: 't4' }),
|
||||
f.relate('Post', 'Tags', { from: 'p1', to: 't1' }),
|
||||
f.relate('Post', 'Tags', { from: 'p2', to: 't2' }),
|
||||
f.relate('Post', 'Tags', { from: 'p3', to: 't3' }),
|
||||
f.relate('Post', 'Tags', { from: 'p4', to: 't4' }),
|
||||
f.relate('Post', 'Tags', { from: 'p5', to: 't1' }),
|
||||
f.relate('Post', 'Tags', { from: 'p6', to: 't2' }),
|
||||
f.relate('Post', 'Tags', { from: 'p7', to: 't3' }),
|
||||
f.relate('Post', 'Tags', { from: 'p8', to: 't4' }),
|
||||
f.relate('Post', 'Tags', { from: 'p9', to: 't1' }),
|
||||
f.relate('Post', 'Tags', { from: 'p10', to: 't2' }),
|
||||
f.relate('Post', 'Tags', { from: 'p11', to: 't3' }),
|
||||
f.relate('Post', 'Tags', { from: 'p12', to: 't4' }),
|
||||
f.relate('Post', 'Tags', { from: 'p13', to: 't1' }),
|
||||
f.relate('Post', 'Tags', { from: 'p14', to: 't2' }),
|
||||
f.relate('Post', 'Tags', { from: 'p15', to: 't3' }),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p0',
|
||||
to: 'Freiheit',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p1',
|
||||
to: 'Umwelt',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p2',
|
||||
to: 'Naturschutz',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p3',
|
||||
to: 'Demokratie',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p4',
|
||||
to: 'Freiheit',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p5',
|
||||
to: 'Umwelt',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p6',
|
||||
to: 'Naturschutz',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p7',
|
||||
to: 'Demokratie',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p8',
|
||||
to: 'Freiheit',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p9',
|
||||
to: 'Umwelt',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p10',
|
||||
to: 'Naturschutz',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p11',
|
||||
to: 'Demokratie',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p12',
|
||||
to: 'Freiheit',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p13',
|
||||
to: 'Umwelt',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p14',
|
||||
to: 'Naturschutz',
|
||||
}),
|
||||
f.relate('Post', 'Tags', {
|
||||
from: 'p15',
|
||||
to: 'Demokratie',
|
||||
}),
|
||||
])
|
||||
|
||||
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([
|
||||
asUser.create('Comment', { id: 'c1', postId: 'p1' }),
|
||||
asTick.create('Comment', { id: 'c2', postId: 'p1' }),
|
||||
asTrack.create('Comment', { id: 'c3', postId: 'p3' }),
|
||||
asTrick.create('Comment', { id: 'c4', postId: 'p2' }),
|
||||
asModerator.create('Comment', { id: 'c5', postId: 'p3' }),
|
||||
asAdmin.create('Comment', { id: 'c6', postId: 'p4' }),
|
||||
asUser.create('Comment', { id: 'c7', postId: 'p2' }),
|
||||
asTick.create('Comment', { id: 'c8', postId: 'p15' }),
|
||||
asTrick.create('Comment', { id: 'c9', postId: 'p15' }),
|
||||
asTrack.create('Comment', { id: 'c10', postId: 'p15' }),
|
||||
asUser.create('Comment', { id: 'c11', postId: 'p15' }),
|
||||
asUser.create('Comment', { id: 'c12', postId: 'p15' }),
|
||||
asUser.create('Comment', {
|
||||
id: 'c1',
|
||||
postId: 'p1',
|
||||
}),
|
||||
asTick.create('Comment', {
|
||||
id: 'c2',
|
||||
postId: 'p1',
|
||||
}),
|
||||
asTrack.create('Comment', {
|
||||
id: 'c3',
|
||||
postId: 'p3',
|
||||
}),
|
||||
asTrick.create('Comment', {
|
||||
id: 'c4',
|
||||
postId: 'p2',
|
||||
}),
|
||||
asModerator.create('Comment', {
|
||||
id: 'c5',
|
||||
postId: 'p3',
|
||||
}),
|
||||
asAdmin.create('Comment', {
|
||||
id: 'c6',
|
||||
postId: 'p4',
|
||||
}),
|
||||
asUser.create('Comment', {
|
||||
id: 'c7',
|
||||
postId: 'p2',
|
||||
}),
|
||||
asTick.create('Comment', {
|
||||
id: 'c8',
|
||||
postId: 'p15',
|
||||
}),
|
||||
asTrick.create('Comment', {
|
||||
id: 'c9',
|
||||
postId: 'p15',
|
||||
}),
|
||||
asTrack.create('Comment', {
|
||||
id: 'c10',
|
||||
postId: 'p15',
|
||||
}),
|
||||
asUser.create('Comment', {
|
||||
id: 'c11',
|
||||
postId: 'p15',
|
||||
}),
|
||||
asUser.create('Comment', {
|
||||
id: 'c12',
|
||||
postId: 'p15',
|
||||
}),
|
||||
])
|
||||
|
||||
const disableMutation = 'mutation($id: ID!) { disable(id: $id) }'
|
||||
await Promise.all([
|
||||
asModerator.mutate(disableMutation, { id: 'p11' }),
|
||||
asModerator.mutate(disableMutation, { id: 'c5' }),
|
||||
asModerator.mutate(disableMutation, {
|
||||
id: 'p11',
|
||||
}),
|
||||
asModerator.mutate(disableMutation, {
|
||||
id: 'c5',
|
||||
}),
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
asTick.create('Report', { description: "I don't like this comment", id: 'c1' }),
|
||||
asTrick.create('Report', { description: "I don't like this post", id: 'p1' }),
|
||||
asTrack.create('Report', { description: "I don't like this user", id: 'u1' }),
|
||||
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([
|
||||
@ -342,13 +666,26 @@ import Factory from './factories'
|
||||
])
|
||||
|
||||
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', 'CreatedBy', {
|
||||
from: 'u1',
|
||||
to: 'o1',
|
||||
}),
|
||||
f.relate('Organization', 'CreatedBy', {
|
||||
from: 'u1',
|
||||
to: 'o2',
|
||||
}),
|
||||
f.relate('Organization', 'OwnedBy', {
|
||||
from: 'u2',
|
||||
to: 'o2',
|
||||
}),
|
||||
f.relate('Organization', 'OwnedBy', {
|
||||
from: 'u2',
|
||||
to: 'o3',
|
||||
}),
|
||||
])
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.log('Seeded Data...')
|
||||
process.exit(0)
|
||||
} catch (err) {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(err)
|
||||
|
||||
@ -9,15 +9,15 @@
|
||||
dependencies:
|
||||
apollo-env "0.5.1"
|
||||
|
||||
"@apollographql/graphql-playground-html@1.6.20":
|
||||
version "1.6.20"
|
||||
resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.20.tgz#bf9f2acdf319c0959fad8ec1239741dd2ead4e8d"
|
||||
integrity sha512-3LWZa80HcP70Pl+H4KhLDJ7S0px+9/c8GTXdl6SpunRecUaB27g/oOQnAjNHLHdbWuGE0uyqcuGiTfbKB3ilaQ==
|
||||
"@apollographql/graphql-playground-html@1.6.24":
|
||||
version "1.6.24"
|
||||
resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc"
|
||||
integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ==
|
||||
|
||||
"@babel/cli@~7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.4.4.tgz#5454bb7112f29026a4069d8e6f0e1794e651966c"
|
||||
integrity sha512-XGr5YjQSjgTa6OzQZY57FAJsdeVSAKR/u/KA5exWIz66IKtv/zXtHy+fIZcMry/EgYegwuHE7vzGnrFhjdIAsQ==
|
||||
"@babel/cli@~7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.5.0.tgz#f403c930692e28ecfa3bf02a9e7562b474f38271"
|
||||
integrity sha512-qNH55fWbKrEsCwID+Qc/3JDPnsSGpIIiMDbppnR8Z6PxLAqMQCFNqBctkIkBrMH49Nx+qqVTrHRWUR+ho2k+qQ==
|
||||
dependencies:
|
||||
commander "^2.8.1"
|
||||
convert-source-map "^1.1.0"
|
||||
@ -38,18 +38,18 @@
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.0.0"
|
||||
|
||||
"@babel/core@^7.1.0", "@babel/core@~7.4.5":
|
||||
version "7.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a"
|
||||
integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA==
|
||||
"@babel/core@^7.1.0", "@babel/core@~7.5.4":
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.4.tgz#4c32df7ad5a58e9ea27ad025c11276324e0b4ddd"
|
||||
integrity sha512-+DaeBEpYq6b2+ZmHx3tHspC+ZRflrvLqwfv8E3hNr5LVQoyBnL8RPKSBCg+rK2W2My9PWlujBiqd0ZPsR9Q6zQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0"
|
||||
"@babel/generator" "^7.4.4"
|
||||
"@babel/helpers" "^7.4.4"
|
||||
"@babel/parser" "^7.4.5"
|
||||
"@babel/generator" "^7.5.0"
|
||||
"@babel/helpers" "^7.5.4"
|
||||
"@babel/parser" "^7.5.0"
|
||||
"@babel/template" "^7.4.4"
|
||||
"@babel/traverse" "^7.4.5"
|
||||
"@babel/types" "^7.4.4"
|
||||
"@babel/traverse" "^7.5.0"
|
||||
"@babel/types" "^7.5.0"
|
||||
convert-source-map "^1.1.0"
|
||||
debug "^4.1.0"
|
||||
json5 "^2.1.0"
|
||||
@ -58,12 +58,12 @@
|
||||
semver "^5.4.1"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/generator@^7.0.0", "@babel/generator@^7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041"
|
||||
integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==
|
||||
"@babel/generator@^7.0.0", "@babel/generator@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.0.tgz#f20e4b7a91750ee8b63656073d843d2a736dca4a"
|
||||
integrity sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.4.4"
|
||||
"@babel/types" "^7.5.0"
|
||||
jsesc "^2.5.1"
|
||||
lodash "^4.17.11"
|
||||
source-map "^0.5.0"
|
||||
@ -260,14 +260,14 @@
|
||||
"@babel/traverse" "^7.1.0"
|
||||
"@babel/types" "^7.2.0"
|
||||
|
||||
"@babel/helpers@^7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5"
|
||||
integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A==
|
||||
"@babel/helpers@^7.5.4":
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.4.tgz#2f00608aa10d460bde0ccf665d6dcf8477357cf0"
|
||||
integrity sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow==
|
||||
dependencies:
|
||||
"@babel/template" "^7.4.4"
|
||||
"@babel/traverse" "^7.4.4"
|
||||
"@babel/types" "^7.4.4"
|
||||
"@babel/traverse" "^7.5.0"
|
||||
"@babel/types" "^7.5.0"
|
||||
|
||||
"@babel/highlight@^7.0.0":
|
||||
version "7.0.0"
|
||||
@ -278,10 +278,10 @@
|
||||
esutils "^2.0.2"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/node@~7.4.5":
|
||||
version "7.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.4.5.tgz#bce71bb44d902bfdd4da0b9c839a8a90fc084056"
|
||||
integrity sha512-nDXPT0KwYMycDHhFG9wKlkipCR+iXzzoX9bD2aF2UABLhQ13AKhNi5Y61W8ASGPPll/7p9GrHesmlOgTUJVcfw==
|
||||
"@babel/node@~7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.5.0.tgz#bcc5a286317ad771703889fb658e1f768c0a2a2e"
|
||||
integrity sha512-VBlCrbJp7HDrKt4HRbtfq4Rs/XjBokvkfxXRQs4qA1C6eV3JycSOMELx4BFTPFRd9QnNA4PsIRfnvJqe/3tHow==
|
||||
dependencies:
|
||||
"@babel/polyfill" "^7.0.0"
|
||||
"@babel/register" "^7.0.0"
|
||||
@ -290,10 +290,10 @@
|
||||
node-environment-flags "^1.0.5"
|
||||
v8flags "^3.1.1"
|
||||
|
||||
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5":
|
||||
version "7.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872"
|
||||
integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==
|
||||
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.4", "@babel/parser@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.0.tgz#3e0713dff89ad6ae37faec3b29dcfc5c979770b7"
|
||||
integrity sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA==
|
||||
|
||||
"@babel/plugin-proposal-async-generator-functions@^7.2.0":
|
||||
version "7.2.0"
|
||||
@ -304,6 +304,14 @@
|
||||
"@babel/helper-remap-async-to-generator" "^7.1.0"
|
||||
"@babel/plugin-syntax-async-generators" "^7.2.0"
|
||||
|
||||
"@babel/plugin-proposal-dynamic-import@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz#e532202db4838723691b10a67b8ce509e397c506"
|
||||
integrity sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
"@babel/plugin-syntax-dynamic-import" "^7.2.0"
|
||||
|
||||
"@babel/plugin-proposal-json-strings@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
|
||||
@ -312,10 +320,10 @@
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
"@babel/plugin-syntax-json-strings" "^7.2.0"
|
||||
|
||||
"@babel/plugin-proposal-object-rest-spread@^7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.4.tgz#1ef173fcf24b3e2df92a678f027673b55e7e3005"
|
||||
integrity sha512-dMBG6cSPBbHeEBdFXeQ2QLc5gUpg4Vkaz8octD4aoW/ISO+jBOcsuxYL7bsb5WSu8RLP6boxrBIALEHgoHtO9g==
|
||||
"@babel/plugin-proposal-object-rest-spread@^7.5.4":
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz#250de35d867ce8260a31b1fdac6c4fc1baa99331"
|
||||
integrity sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
|
||||
@ -352,6 +360,13 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
"@babel/plugin-syntax-dynamic-import@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612"
|
||||
integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
"@babel/plugin-syntax-json-strings@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470"
|
||||
@ -387,10 +402,10 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
"@babel/plugin-transform-async-to-generator@^7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.4.tgz#a3f1d01f2f21cadab20b33a82133116f14fb5894"
|
||||
integrity sha512-YiqW2Li8TXmzgbXw+STsSqPBPFnGviiaSp6CYOq55X8GQ2SGVLrXB6pNid8HkqkZAzOH6knbai3snhP7v0fNwA==
|
||||
"@babel/plugin-transform-async-to-generator@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz#89a3848a0166623b5bc481164b5936ab947e887e"
|
||||
integrity sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.0.0"
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
@ -432,10 +447,10 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
"@babel/plugin-transform-destructuring@^7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.4.tgz#9d964717829cc9e4b601fc82a26a71a4d8faf20f"
|
||||
integrity sha512-/aOx+nW0w8eHiEHm+BTERB2oJn5D127iye/SUQl7NjHy0lf+j7h4MKMMSOwdazGq9OxgiNADncE+SRJkCxjZpQ==
|
||||
"@babel/plugin-transform-destructuring@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz#f6c09fdfe3f94516ff074fe877db7bc9ef05855a"
|
||||
integrity sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
@ -448,10 +463,10 @@
|
||||
"@babel/helper-regex" "^7.4.4"
|
||||
regexpu-core "^4.5.4"
|
||||
|
||||
"@babel/plugin-transform-duplicate-keys@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz#d952c4930f312a4dbfff18f0b2914e60c35530b3"
|
||||
integrity sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw==
|
||||
"@babel/plugin-transform-duplicate-keys@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz#c5dbf5106bf84cdf691222c0974c12b1df931853"
|
||||
integrity sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
@ -492,30 +507,33 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
"@babel/plugin-transform-modules-amd@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz#82a9bce45b95441f617a24011dc89d12da7f4ee6"
|
||||
integrity sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw==
|
||||
"@babel/plugin-transform-modules-amd@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz#ef00435d46da0a5961aa728a1d2ecff063e4fb91"
|
||||
integrity sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==
|
||||
dependencies:
|
||||
"@babel/helper-module-transforms" "^7.1.0"
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
babel-plugin-dynamic-import-node "^2.3.0"
|
||||
|
||||
"@babel/plugin-transform-modules-commonjs@^7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.4.tgz#0bef4713d30f1d78c2e59b3d6db40e60192cac1e"
|
||||
integrity sha512-4sfBOJt58sEo9a2BQXnZq+Q3ZTSAUXyK3E30o36BOGnJ+tvJ6YSxF0PG6kERvbeISgProodWuI9UVG3/FMY6iw==
|
||||
"@babel/plugin-transform-modules-commonjs@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74"
|
||||
integrity sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ==
|
||||
dependencies:
|
||||
"@babel/helper-module-transforms" "^7.4.4"
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
"@babel/helper-simple-access" "^7.1.0"
|
||||
babel-plugin-dynamic-import-node "^2.3.0"
|
||||
|
||||
"@babel/plugin-transform-modules-systemjs@^7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.4.tgz#dc83c5665b07d6c2a7b224c00ac63659ea36a405"
|
||||
integrity sha512-MSiModfILQc3/oqnG7NrP1jHaSPryO6tA2kOMmAQApz5dayPxWiHqmq4sWH2xF5LcQK56LlbKByCd8Aah/OIkQ==
|
||||
"@babel/plugin-transform-modules-systemjs@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz#e75266a13ef94202db2a0620977756f51d52d249"
|
||||
integrity sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg==
|
||||
dependencies:
|
||||
"@babel/helper-hoist-variables" "^7.4.4"
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
babel-plugin-dynamic-import-node "^2.3.0"
|
||||
|
||||
"@babel/plugin-transform-modules-umd@^7.2.0":
|
||||
version "7.2.0"
|
||||
@ -631,39 +649,41 @@
|
||||
core-js "^2.5.7"
|
||||
regenerator-runtime "^0.12.0"
|
||||
|
||||
"@babel/preset-env@~7.4.5":
|
||||
version "7.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58"
|
||||
integrity sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w==
|
||||
"@babel/preset-env@~7.5.4":
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.4.tgz#64bc15041a3cbb0798930319917e70fcca57713d"
|
||||
integrity sha512-hFnFnouyRNiH1rL8YkX1ANCNAUVC8Djwdqfev8i1415tnAG+7hlA5zhZ0Q/3Q5gkop4HioIPbCEWAalqcbxRoQ==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.0.0"
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
"@babel/plugin-proposal-async-generator-functions" "^7.2.0"
|
||||
"@babel/plugin-proposal-dynamic-import" "^7.5.0"
|
||||
"@babel/plugin-proposal-json-strings" "^7.2.0"
|
||||
"@babel/plugin-proposal-object-rest-spread" "^7.4.4"
|
||||
"@babel/plugin-proposal-object-rest-spread" "^7.5.4"
|
||||
"@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
|
||||
"@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
|
||||
"@babel/plugin-syntax-async-generators" "^7.2.0"
|
||||
"@babel/plugin-syntax-dynamic-import" "^7.2.0"
|
||||
"@babel/plugin-syntax-json-strings" "^7.2.0"
|
||||
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
|
||||
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
|
||||
"@babel/plugin-transform-arrow-functions" "^7.2.0"
|
||||
"@babel/plugin-transform-async-to-generator" "^7.4.4"
|
||||
"@babel/plugin-transform-async-to-generator" "^7.5.0"
|
||||
"@babel/plugin-transform-block-scoped-functions" "^7.2.0"
|
||||
"@babel/plugin-transform-block-scoping" "^7.4.4"
|
||||
"@babel/plugin-transform-classes" "^7.4.4"
|
||||
"@babel/plugin-transform-computed-properties" "^7.2.0"
|
||||
"@babel/plugin-transform-destructuring" "^7.4.4"
|
||||
"@babel/plugin-transform-destructuring" "^7.5.0"
|
||||
"@babel/plugin-transform-dotall-regex" "^7.4.4"
|
||||
"@babel/plugin-transform-duplicate-keys" "^7.2.0"
|
||||
"@babel/plugin-transform-duplicate-keys" "^7.5.0"
|
||||
"@babel/plugin-transform-exponentiation-operator" "^7.2.0"
|
||||
"@babel/plugin-transform-for-of" "^7.4.4"
|
||||
"@babel/plugin-transform-function-name" "^7.4.4"
|
||||
"@babel/plugin-transform-literals" "^7.2.0"
|
||||
"@babel/plugin-transform-member-expression-literals" "^7.2.0"
|
||||
"@babel/plugin-transform-modules-amd" "^7.2.0"
|
||||
"@babel/plugin-transform-modules-commonjs" "^7.4.4"
|
||||
"@babel/plugin-transform-modules-systemjs" "^7.4.4"
|
||||
"@babel/plugin-transform-modules-amd" "^7.5.0"
|
||||
"@babel/plugin-transform-modules-commonjs" "^7.5.0"
|
||||
"@babel/plugin-transform-modules-systemjs" "^7.5.0"
|
||||
"@babel/plugin-transform-modules-umd" "^7.2.0"
|
||||
"@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5"
|
||||
"@babel/plugin-transform-new-target" "^7.4.4"
|
||||
@ -678,7 +698,7 @@
|
||||
"@babel/plugin-transform-template-literals" "^7.4.4"
|
||||
"@babel/plugin-transform-typeof-symbol" "^7.2.0"
|
||||
"@babel/plugin-transform-unicode-regex" "^7.4.4"
|
||||
"@babel/types" "^7.4.4"
|
||||
"@babel/types" "^7.5.0"
|
||||
browserslist "^4.6.0"
|
||||
core-js-compat "^3.1.1"
|
||||
invariant "^2.2.2"
|
||||
@ -704,6 +724,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.4.4":
|
||||
version "7.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
|
||||
integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237"
|
||||
@ -713,25 +740,25 @@
|
||||
"@babel/parser" "^7.4.4"
|
||||
"@babel/types" "^7.4.4"
|
||||
|
||||
"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5":
|
||||
version "7.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216"
|
||||
integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==
|
||||
"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.0.tgz#4216d6586854ef5c3c4592dab56ec7eb78485485"
|
||||
integrity sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0"
|
||||
"@babel/generator" "^7.4.4"
|
||||
"@babel/generator" "^7.5.0"
|
||||
"@babel/helper-function-name" "^7.1.0"
|
||||
"@babel/helper-split-export-declaration" "^7.4.4"
|
||||
"@babel/parser" "^7.4.5"
|
||||
"@babel/types" "^7.4.4"
|
||||
"@babel/parser" "^7.5.0"
|
||||
"@babel/types" "^7.5.0"
|
||||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
lodash "^4.17.11"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0"
|
||||
integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4", "@babel/types@^7.5.0":
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.0.tgz#e47d43840c2e7f9105bc4d3a2c371b4d0c7832ab"
|
||||
integrity sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ==
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
lodash "^4.17.11"
|
||||
@ -745,6 +772,43 @@
|
||||
exec-sh "^0.3.2"
|
||||
minimist "^1.2.0"
|
||||
|
||||
"@hapi/address@2.x.x":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.0.0.tgz#9f05469c88cb2fd3dcd624776b54ee95c312126a"
|
||||
integrity sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw==
|
||||
|
||||
"@hapi/hoek@6.x.x":
|
||||
version "6.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-6.2.4.tgz#4b95fbaccbfba90185690890bdf1a2fbbda10595"
|
||||
integrity sha512-HOJ20Kc93DkDVvjwHyHawPwPkX44sIrbXazAUDiUXaY2R9JwQGo2PhFfnQtdrsIe4igjG2fPgMra7NYw7qhy0A==
|
||||
|
||||
"@hapi/hoek@8.x.x":
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.0.1.tgz#9712fa2ad124ac64668ab06ba847b1eaf83a03fd"
|
||||
integrity sha512-cctMYH5RLbElaUpZn3IJaUj9QNQD8iXDnl7xNY6KB1aFD2ciJrwpo3kvZowIT75uA+silJFDnSR2kGakALUymg==
|
||||
|
||||
"@hapi/joi@^15.0.3", "@hapi/joi@^15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.0.tgz#940cb749b5c55c26ab3b34ce362e82b6162c8e7a"
|
||||
integrity sha512-n6kaRQO8S+kepUTbXL9O/UOL788Odqs38/VOfoCrATDtTvyfiO3fgjlSRaNkHabpTLgM7qru9ifqXlXbXk8SeQ==
|
||||
dependencies:
|
||||
"@hapi/address" "2.x.x"
|
||||
"@hapi/hoek" "6.x.x"
|
||||
"@hapi/marker" "1.x.x"
|
||||
"@hapi/topo" "3.x.x"
|
||||
|
||||
"@hapi/marker@1.x.x":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/marker/-/marker-1.0.0.tgz#65b0b2b01d1be06304886ce9b4b77b1bfb21a769"
|
||||
integrity sha512-JOfdekTXnJexfE8PyhZFyHvHjt81rBFSAbTIRAhF2vv/2Y1JzoKsGqxH/GpZJoF7aEfYok8JVcAHmSz1gkBieA==
|
||||
|
||||
"@hapi/topo@3.x.x":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.2.tgz#57cc1317be1a8c5f47c124f9b0e3c49cd78424d2"
|
||||
integrity sha512-r+aumOqJ5QbD6aLPJWqVjMAPsx5pZKz+F5yPqXZ/WWG9JTtHbQqlzrJoknJ0iJxLj9vlXtmpSdjlkszseeG8OA==
|
||||
dependencies:
|
||||
"@hapi/hoek" "8.x.x"
|
||||
|
||||
"@jest/console@^24.7.1":
|
||||
version "24.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545"
|
||||
@ -1110,10 +1174,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.17":
|
||||
version "0.26.17"
|
||||
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.17.tgz#5cb7cfc211d8e985b21d88289542591c92cad9dc"
|
||||
integrity sha512-MN7VHlPsZQ2MTBxLE2Gl+Qfg2WyKsoz+vIr8xN0OSZ4AvJDrrKBlxc8b59UXCCIG9tPn9XhxTXh3j/htHbzC2Q==
|
||||
"@types/yup@0.26.21":
|
||||
version "0.26.21"
|
||||
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.21.tgz#bfca27a02a0631495bfd25b6c63647a125e6944e"
|
||||
integrity sha512-1C45M7hZrVsl8bXxYV01bitRp0r35djt+eX5HWFH3NdH+8ejqta3KM7rmQLRLrupkhF7jRkAtXl2EgDsriIqwA==
|
||||
|
||||
"@types/zen-observable@^0.5.3":
|
||||
version "0.5.4"
|
||||
@ -1279,13 +1343,13 @@ anymatch@^2.0.0:
|
||||
micromatch "^3.1.4"
|
||||
normalize-path "^2.1.1"
|
||||
|
||||
apollo-cache-control@0.7.4:
|
||||
version "0.7.4"
|
||||
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.4.tgz#0cb5c7be0e0dd0c44b1257144cd7f9f2a3c374e6"
|
||||
integrity sha512-TVACHwcEF4wfHo5H9FLnoNjo0SLDo2jPW+bXs9aw0Y4Z2UisskSAPnIYOqUPnU8SoeNvs7zWgbLizq11SRTJtg==
|
||||
apollo-cache-control@0.7.5:
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.5.tgz#5d8b949bd9b4f03ca32c7d7e429f509c6881eefc"
|
||||
integrity sha512-zCPwHjbo/VlmXl0sclZfBq/MlVVeGUAg02Q259OIXSgHBvn9BbExyz+EkO/DJvZfGMquxqS1X1BFO3VKuLUTdw==
|
||||
dependencies:
|
||||
apollo-server-env "2.4.0"
|
||||
graphql-extensions "0.7.4"
|
||||
graphql-extensions "0.7.7"
|
||||
|
||||
apollo-cache-control@^0.1.0:
|
||||
version "0.1.1"
|
||||
@ -1342,17 +1406,17 @@ apollo-engine-reporting-protobuf@0.3.1:
|
||||
dependencies:
|
||||
protobufjs "^6.8.6"
|
||||
|
||||
apollo-engine-reporting@1.3.4:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.4.tgz#65e12f94221d80b3b1740c26e82ce9bb6bdfb7ee"
|
||||
integrity sha512-DJdYghyUBzT0/LcPLwuQNXDCw06r1RfxkVfNTGKoTv6a+leVvjhDJmXvc+jSuBPwaNsc+RYRnfyQ2qUn9fmfyA==
|
||||
apollo-engine-reporting@1.3.6:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.6.tgz#579ba2da85ff848bd92be1b0f1ad61f0c57e3585"
|
||||
integrity sha512-oCoFAUBGveg1i1Sao/2gNsf1kirJBT6vw6Zan9BCNUkyh68ewDts+xRg32VnD9lDhaHpXVJ3tVtuaV44HmdSEw==
|
||||
dependencies:
|
||||
apollo-engine-reporting-protobuf "0.3.1"
|
||||
apollo-graphql "^0.3.2"
|
||||
apollo-server-core "2.6.6"
|
||||
apollo-graphql "^0.3.3"
|
||||
apollo-server-core "2.6.9"
|
||||
apollo-server-env "2.4.0"
|
||||
async-retry "^1.2.1"
|
||||
graphql-extensions "0.7.5"
|
||||
graphql-extensions "0.7.7"
|
||||
|
||||
apollo-env@0.5.1:
|
||||
version "0.5.1"
|
||||
@ -1371,10 +1435,10 @@ apollo-errors@^1.9.0:
|
||||
assert "^1.4.1"
|
||||
extendable-error "^0.1.5"
|
||||
|
||||
apollo-graphql@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.2.tgz#8881a87f1d5fcf80837b34dba90737e664eabe9a"
|
||||
integrity sha512-YbzYGR14GV0023m//EU66vOzZ3i7c04V/SF8Qk+60vf1sOWyKgO6mxZJ4BKhw10qWUayirhSDxq3frYE+qSG0A==
|
||||
apollo-graphql@^0.3.3:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.3.tgz#ce1df194f6e547ad3ce1e35b42f9c211766e1658"
|
||||
integrity sha512-t3CO/xIDVsCG2qOvx2MEbuu4b/6LzQjcBBwiVnxclmmFyAxYCIe7rpPlnLHSq7HyOMlCWDMozjoeWfdqYSaLqQ==
|
||||
dependencies:
|
||||
apollo-env "0.5.1"
|
||||
lodash.sortby "^4.7.0"
|
||||
@ -1422,24 +1486,24 @@ apollo-server-caching@0.4.0:
|
||||
dependencies:
|
||||
lru-cache "^5.0.0"
|
||||
|
||||
apollo-server-core@2.6.6:
|
||||
version "2.6.6"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.6.tgz#55fea7980a943948c49dea20d81b9bbfc0e04f87"
|
||||
integrity sha512-PFSjJbqkV1eetfFJxu11gzklQYC8BrF0RZfvC1d1mhvtxAOKl25uhPHxltN0Omyjp7LW4YeoC6zwl9rLWuhZFQ==
|
||||
apollo-server-core@2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.9.tgz#75542ad206782e5c31a023b54962e9fdc6404a91"
|
||||
integrity sha512-r2/Kjm1UmxoTViUt5EcExWXkWl0riXsuGyS1q5LpHKKnA+6b+t4LQKECkRU4EWNpuuzJQn7aF7MmMdvURxoEig==
|
||||
dependencies:
|
||||
"@apollographql/apollo-tools" "^0.3.6"
|
||||
"@apollographql/graphql-playground-html" "1.6.20"
|
||||
"@apollographql/graphql-playground-html" "1.6.24"
|
||||
"@types/ws" "^6.0.0"
|
||||
apollo-cache-control "0.7.4"
|
||||
apollo-cache-control "0.7.5"
|
||||
apollo-datasource "0.5.0"
|
||||
apollo-engine-reporting "1.3.4"
|
||||
apollo-engine-reporting "1.3.6"
|
||||
apollo-server-caching "0.4.0"
|
||||
apollo-server-env "2.4.0"
|
||||
apollo-server-errors "2.3.0"
|
||||
apollo-server-plugin-base "0.5.5"
|
||||
apollo-tracing "0.7.3"
|
||||
apollo-server-errors "2.3.1"
|
||||
apollo-server-plugin-base "0.5.8"
|
||||
apollo-tracing "0.7.4"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
graphql-extensions "0.7.5"
|
||||
graphql-extensions "0.7.7"
|
||||
graphql-subscriptions "^1.0.0"
|
||||
graphql-tag "^2.9.2"
|
||||
graphql-tools "^4.0.0"
|
||||
@ -1465,23 +1529,23 @@ apollo-server-env@2.4.0:
|
||||
node-fetch "^2.1.2"
|
||||
util.promisify "^1.0.0"
|
||||
|
||||
apollo-server-errors@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061"
|
||||
integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw==
|
||||
apollo-server-errors@2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.1.tgz#033cf331463ebb99a563f8354180b41ac6714eb6"
|
||||
integrity sha512-errZvnh0vUQChecT7M4A/h94dnBSRL213dNxpM5ueMypaLYgnp4hiCTWIEaooo9E4yMGd1qA6WaNbLDG2+bjcg==
|
||||
|
||||
apollo-server-express@2.6.6:
|
||||
version "2.6.6"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.6.tgz#ec2b955354d7dd4d12fe01ea7e983d302071d5b9"
|
||||
integrity sha512-bY/xrr9lZH+hsjchiQuSXpW3ivXfL1h81M5VE9Ppus1PVwwEIar/irBN+PFp97WxERZPDjVZzrRKa+lRHjtJsA==
|
||||
apollo-server-express@2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.9.tgz#176dab7f2cd5a99655c8eb382ad9b10797422a7b"
|
||||
integrity sha512-iTkdIdX7m9EAlmL/ZPkKR+x/xuFk1HYZWuJIJG57hHUhcOxj50u7F1E5+5fDwl5RFIdepQ61azF31hhNZuNi4g==
|
||||
dependencies:
|
||||
"@apollographql/graphql-playground-html" "1.6.20"
|
||||
"@apollographql/graphql-playground-html" "1.6.24"
|
||||
"@types/accepts" "^1.3.5"
|
||||
"@types/body-parser" "1.17.0"
|
||||
"@types/cors" "^2.8.4"
|
||||
"@types/express" "4.17.0"
|
||||
accepts "^1.3.5"
|
||||
apollo-server-core "2.6.6"
|
||||
apollo-server-core "2.6.9"
|
||||
body-parser "^1.18.3"
|
||||
cors "^2.8.4"
|
||||
graphql-subscriptions "^1.0.0"
|
||||
@ -1509,36 +1573,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.5.5:
|
||||
version "0.5.5"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.5.tgz#364e4a2fca4d95ddeb9fd3e78940ed1da58865c2"
|
||||
integrity sha512-agiuhknyu3lnnEsqUh99tzxwPCGp+TuDK+TSRTkXU1RUG6lY4C3uJp0JGJw03cP+M6ze73TbRjMA4E68g/ks5A==
|
||||
apollo-server-plugin-base@0.5.8:
|
||||
version "0.5.8"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.8.tgz#77b4127aff4e3514a9d49e3cc61256aee4d9422e"
|
||||
integrity sha512-ICbaXr0ycQZL5llbtZhg8zyHbxuZ4khdAJsJgiZaUXXP6+F47XfDQ5uwnl/4Sq9fvkpwS0ctvfZ1D+Ks4NvUzA==
|
||||
|
||||
apollo-server-testing@~2.6.6:
|
||||
version "2.6.6"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.6.tgz#3d96486ebdb151219183fcb715973a8385c66e0a"
|
||||
integrity sha512-GfzEAqXGzhWp1YgNJONAijC3mC34E6cI5XiOdLX9FGAnBmZvDURlZwloZbdNgvqvXnwuxuNgo4xvCnTe7kndqg==
|
||||
apollo-server-testing@~2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.9.tgz#6c1d20a89c0676bf32714405d729c302d62adfb1"
|
||||
integrity sha512-MQfXAjNsI63O9sY60tQnGy102sqJSr++Yzm+IR44WrK3Z7FHUDisoh6UATly04EDGtO034xtqukzdUNQCK7+rw==
|
||||
dependencies:
|
||||
apollo-server-core "2.6.6"
|
||||
apollo-server-core "2.6.9"
|
||||
|
||||
apollo-server@~2.6.6:
|
||||
version "2.6.6"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.6.tgz#0570fce4a682eb1de8bc1b86dbe2543de440cd4e"
|
||||
integrity sha512-7Bulb3RnOO4/SGA66LXu3ZHCXIK8MYMrsxy4yti1/adDIUmcniolDqJwOYUGoTmv1AQjRxgJb4TVZ0Dk9nrrYg==
|
||||
apollo-server@~2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.9.tgz#10e70488b35bf5171612dfd3f030e4ef94c75295"
|
||||
integrity sha512-thZxUHVM1CLl3503gMCVirxN9J/33s5C1R+hHMEfLaUSoDlXSMA81Y9LCOi9+6d0C9l5DwiZCFXeZI/fKic2RA==
|
||||
dependencies:
|
||||
apollo-server-core "2.6.6"
|
||||
apollo-server-express "2.6.6"
|
||||
apollo-server-core "2.6.9"
|
||||
apollo-server-express "2.6.9"
|
||||
express "^4.0.0"
|
||||
graphql-subscriptions "^1.0.0"
|
||||
graphql-tools "^4.0.0"
|
||||
|
||||
apollo-tracing@0.7.3:
|
||||
version "0.7.3"
|
||||
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.3.tgz#8533e3e2dca2d5a25e8439ce498ea33ff4d159ee"
|
||||
integrity sha512-H6fSC+awQGnfDyYdGIB0UQUhcUC3n5Vy+ujacJ0bY6R+vwWeZOQvu7wRHNjk/rbOSTLCo9A0OcVX7huRyu9SZg==
|
||||
apollo-tracing@0.7.4:
|
||||
version "0.7.4"
|
||||
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.4.tgz#f24d1065100b6d8bf581202859ea0e85ba7bf30d"
|
||||
integrity sha512-vA0FJCBkFpwdWyVF5UtCqN+enShejyiqSGqq8NxXHU1+GEYTngWa56x9OGsyhX+z4aoDIa3HPKPnP3pjzA0qpg==
|
||||
dependencies:
|
||||
apollo-server-env "2.4.0"
|
||||
graphql-extensions "0.7.4"
|
||||
graphql-extensions "0.7.7"
|
||||
|
||||
apollo-tracing@^0.1.0:
|
||||
version "0.1.4"
|
||||
@ -1806,6 +1870,13 @@ babel-jest@^24.8.0, babel-jest@~24.8.0:
|
||||
chalk "^2.4.2"
|
||||
slash "^2.0.0"
|
||||
|
||||
babel-plugin-dynamic-import-node@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
|
||||
integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==
|
||||
dependencies:
|
||||
object.assign "^4.1.0"
|
||||
|
||||
babel-plugin-istanbul@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.0.tgz#6892f529eff65a3e2d33d87dc5888ffa2ecd4a30"
|
||||
@ -2406,17 +2477,12 @@ core-js-pure@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@^2.4.0, core-js@^2.5.3, core-js@^2.5.7, core-js@^2.6.5:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
|
||||
integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
|
||||
|
||||
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:
|
||||
core-js@^3.0.0, 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==
|
||||
@ -2584,10 +2650,10 @@ data-urls@^1.0.0:
|
||||
whatwg-mimetype "^2.2.0"
|
||||
whatwg-url "^7.0.0"
|
||||
|
||||
date-fns@2.0.0-alpha.37:
|
||||
version "2.0.0-alpha.37"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.37.tgz#c58b3e827da4f860ec8dc123e54019efb4a610e0"
|
||||
integrity sha512-fyIv/h6fkFd1u2NHXni5LPRPoa9FFh3hY67JSjNfa+k/u4EKvfrpGtoTM16Y/BJOqTb4W05UjcmwBna1ElyxDA==
|
||||
date-fns@2.0.0-beta.1:
|
||||
version "2.0.0-beta.1"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-beta.1.tgz#6f3209ea8be559211be5160e0a6379a7eade227b"
|
||||
integrity sha512-ls5W/PUZmrtck53HD3Sd0564NlnNoQtcxNCwWcIzULJMNNgAPVKHoylVXPau7vdyu5/JTd25ljtan+iWnnUKkw==
|
||||
|
||||
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"
|
||||
@ -2816,6 +2882,11 @@ dotenv@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-0.4.0.tgz#f6fb351363c2d92207245c737802c9ab5ae1495a"
|
||||
integrity sha1-9vs1E2PC2SIHJFxzeALJq1rhSVo=
|
||||
|
||||
dotenv@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
|
||||
integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=
|
||||
|
||||
dotenv@~8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440"
|
||||
@ -3000,10 +3071,10 @@ escodegen@^1.9.1:
|
||||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
eslint-config-prettier@~5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-5.0.0.tgz#f7a94b2b8ae7cbf25842c36fa96c6d32cd0a697c"
|
||||
integrity sha512-c17Aqiz5e8LEqoc/QPmYnaxQFAHTx2KlCZBPxXXjEMmNchOLnV/7j0HoPZuC+rL/tDC9bazUYOKJW9bOhftI/w==
|
||||
eslint-config-prettier@~6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.0.0.tgz#f429a53bde9fc7660e6353910fd996d6284d3c25"
|
||||
integrity sha512-vDrcCFE3+2ixNT5H83g28bO/uYAwibJxerXPj+E7op4qzBCsAV36QfvdAyVOoNxKAH2Os/e01T/2x++V0LPukA==
|
||||
dependencies:
|
||||
get-stdin "^6.0.0"
|
||||
|
||||
@ -3053,10 +3124,10 @@ eslint-plugin-import@~2.18.0:
|
||||
read-pkg-up "^2.0.0"
|
||||
resolve "^1.11.0"
|
||||
|
||||
eslint-plugin-jest@~22.7.1:
|
||||
version "22.7.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.1.tgz#5dcdf8f7a285f98040378220d6beca581f0ab2a1"
|
||||
integrity sha512-CrT3AzA738neimv8G8iK2HCkrCwHnAJeeo7k5TEHK86VMItKl6zdJT/tHBDImfnVVAYsVs4Y6BUdBZQCCgfiyw==
|
||||
eslint-plugin-jest@~22.7.2:
|
||||
version "22.7.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.2.tgz#7ab118a66a34e46ae5e16a128b5d24fd28b43dca"
|
||||
integrity sha512-Aecqe3ulBVI7amgOycVI8ZPL8o0SnGHOf3zn2/Ciu8TXyXDHcjtwD3hOs3ss/Qh/VAwlW/DMcuiXg5btgF+XMA==
|
||||
|
||||
eslint-plugin-node@~9.1.0:
|
||||
version "9.1.0"
|
||||
@ -3720,19 +3791,13 @@ 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.7.4:
|
||||
version "0.7.4"
|
||||
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.4.tgz#78327712822281d5778b9210a55dc59c93a9c184"
|
||||
integrity sha512-Ly+DiTDU+UtlfPGQkqmBX2SWMr9OT3JxMRwpB9K86rDNDBTJtG6AE2kliQKKE+hg1+945KAimO7Ep+YAvS7ywg==
|
||||
dependencies:
|
||||
"@apollographql/apollo-tools" "^0.3.6"
|
||||
|
||||
graphql-extensions@0.7.5:
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.5.tgz#fab2b9e53cf6014952e6547456d50680ff0ea579"
|
||||
integrity sha512-B1m+/WEJa3IYKWqBPS9W/7OasfPmlHOSz5hpEAq2Jbn6T0FQ/d2YWFf2HBETHR3RR2qfT+55VMiYovl2ga3qcg==
|
||||
graphql-extensions@0.7.7:
|
||||
version "0.7.7"
|
||||
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.7.tgz#19f4dea35391065de72b25def98f8396887bdf43"
|
||||
integrity sha512-xiTbVGPUpLbF86Bc+zxI/v/axRkwZx3s+y2/kUb2c2MxNZeNhMZEw1dSutuhY2f2JkRkYFJii0ucjIVqPAQ/Lg==
|
||||
dependencies:
|
||||
"@apollographql/apollo-tools" "^0.3.6"
|
||||
apollo-server-env "2.4.0"
|
||||
|
||||
graphql-extensions@^0.0.x, graphql-extensions@~0.0.9:
|
||||
version "0.0.10"
|
||||
@ -3788,12 +3853,12 @@ graphql-request@~1.8.2:
|
||||
dependencies:
|
||||
cross-fetch "2.2.2"
|
||||
|
||||
graphql-shield@~5.7.1:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.7.1.tgz#04095fb8148a463997f7c509d4aeb2a6abf79f98"
|
||||
integrity sha512-UZ0K1uAqRAoGA1U2DsUu4vIZX2Vents4Xim99GFEUBTgvSDkejiE+k/Dywqfu76lJFEE8qu3vG5fhJN3SmnKbA==
|
||||
graphql-shield@~6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-6.0.3.tgz#a79ca8b2fe58fb9558ffc0e64fa8aa19f63af1b3"
|
||||
integrity sha512-+yVT/dRWsRqeJOTHcEElJVfvIRPrhBqPlg5FHLmSkWNdGMR4AFqAQGrJteuZuNDvJ3bt380CZ96Bvf4J9hUpKA==
|
||||
dependencies:
|
||||
"@types/yup" "0.26.17"
|
||||
"@types/yup" "0.26.21"
|
||||
lightercollective "^0.3.0"
|
||||
object-hash "^1.3.1"
|
||||
yup "^0.27.0"
|
||||
@ -3866,10 +3931,10 @@ graphql-yoga@~1.18.0:
|
||||
graphql-upload "^8.0.0"
|
||||
subscriptions-transport-ws "^0.9.8"
|
||||
|
||||
"graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", graphql@^14.2.1, graphql@~14.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==
|
||||
"graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", graphql@^14.2.1, graphql@~14.4.2:
|
||||
version "14.4.2"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.2.tgz#553a7d546d524663eda49ed6df77577be3203ae3"
|
||||
integrity sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ==
|
||||
dependencies:
|
||||
iterall "^1.2.2"
|
||||
|
||||
@ -4916,7 +4981,7 @@ jmespath@0.15.0:
|
||||
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
|
||||
integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
|
||||
|
||||
joi@^13.0.0:
|
||||
joi@^13.7.0:
|
||||
version "13.7.0"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f"
|
||||
integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==
|
||||
@ -5275,9 +5340,9 @@ lodash.isstring@^4.0.1:
|
||||
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
|
||||
|
||||
lodash.mergewith@^4.6.1:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
|
||||
integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
|
||||
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
|
||||
|
||||
lodash.once@^4.0.0:
|
||||
version "4.1.1"
|
||||
@ -5294,10 +5359,10 @@ lodash@=3.10.1:
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
|
||||
integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
|
||||
|
||||
lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.11:
|
||||
version "4.17.11"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||
lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.14:
|
||||
version "4.17.14"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
|
||||
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
|
||||
|
||||
long@^4.0.0:
|
||||
version "4.0.0"
|
||||
@ -5604,6 +5669,15 @@ negotiator@0.6.2:
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
||||
|
||||
neo4j-driver@^1.6.3:
|
||||
version "1.7.5"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.5.tgz#c3fe3677f69c12f26944563d45e7e7d818a685e4"
|
||||
integrity sha512-xCD2F5+tp/SD9r5avX5bSoY8u8RH2o793xJ9Ikjz1s5qQy7cFxFbbj2c52uz3BVGhRAx/NmB57VjOquYmmxGtw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.4"
|
||||
text-encoding-utf-8 "^1.0.2"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
neo4j-driver@^1.7.3, neo4j-driver@~1.7.4:
|
||||
version "1.7.4"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.4.tgz#9661cf643b63818bff85e82c4691918e75098c1e"
|
||||
@ -5623,6 +5697,16 @@ neo4j-graphql-js@^2.6.3:
|
||||
lodash "^4.17.11"
|
||||
neo4j-driver "^1.7.3"
|
||||
|
||||
neode@^0.2.16:
|
||||
version "0.2.16"
|
||||
resolved "https://registry.yarnpkg.com/neode/-/neode-0.2.16.tgz#20532cc67604fd00cc88de841d422f5238ae5bd3"
|
||||
integrity sha512-L9p55IDKGzAZsQgHdXrfd2xasDuB46RipcrPw6NP7ESgkmfJMaMWRZ1F3Kv+f4V4U1WnhZ1IILvwVFhYPnpXEg==
|
||||
dependencies:
|
||||
dotenv "^4.0.0"
|
||||
joi "^13.7.0"
|
||||
neo4j-driver "^1.6.3"
|
||||
uuid "^3.3.2"
|
||||
|
||||
next-tick@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
||||
@ -5711,10 +5795,10 @@ node-releases@^1.1.19:
|
||||
dependencies:
|
||||
semver "^5.3.0"
|
||||
|
||||
nodemailer@^6.2.1:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.2.1.tgz#20d773925eb8f7a06166a0b62c751dc8290429f3"
|
||||
integrity sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g==
|
||||
nodemailer@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.0.tgz#a89b0c62d3937bdcdeecbf55687bd7911b627e12"
|
||||
integrity sha512-TEHBNBPHv7Ie/0o3HXnb7xrPSSQmH1dXwQKRaMKDBGt/ZN54lvDVujP6hKkO/vjkIYL9rK8kHSG11+G42Nhxuw==
|
||||
|
||||
nodemon@~1.19.1:
|
||||
version "1.19.1"
|
||||
@ -5860,6 +5944,11 @@ object-hash@^1.3.1:
|
||||
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
|
||||
integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
|
||||
|
||||
object-keys@^1.0.11:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||
|
||||
object-keys@^1.0.12:
|
||||
version "1.0.12"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2"
|
||||
@ -5877,6 +5966,16 @@ object-visit@^1.0.0:
|
||||
dependencies:
|
||||
isobject "^3.0.0"
|
||||
|
||||
object.assign@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
|
||||
integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
|
||||
dependencies:
|
||||
define-properties "^1.1.2"
|
||||
function-bind "^1.1.1"
|
||||
has-symbols "^1.0.0"
|
||||
object-keys "^1.0.11"
|
||||
|
||||
object.getownpropertydescriptors@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
|
||||
@ -7411,6 +7510,11 @@ test-exclude@^5.0.0:
|
||||
read-pkg-up "^4.0.0"
|
||||
require-main-filename "^1.0.1"
|
||||
|
||||
text-encoding-utf-8@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13"
|
||||
integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==
|
||||
|
||||
text-encoding@^0.6.4:
|
||||
version "0.6.4"
|
||||
resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
|
||||
@ -7891,13 +7995,13 @@ w3c-hr-time@^1.0.1:
|
||||
dependencies:
|
||||
browser-process-hrtime "^0.1.2"
|
||||
|
||||
wait-on@~3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-3.2.0.tgz#c83924df0fc42a675c678324c49c769d378bcb85"
|
||||
integrity sha512-QUGNKlKLDyY6W/qHdxaRlXUAgLPe+3mLL/tRByHpRNcHs/c7dZXbu+OnJWGNux6tU1WFh/Z8aEwvbuzSAu79Zg==
|
||||
wait-on@~3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-3.3.0.tgz#9940981d047a72a9544a97b8b5fca45b2170a082"
|
||||
integrity sha512-97dEuUapx4+Y12aknWZn7D25kkjMk16PbWoYzpSdA8bYpVfS6hpl2a2pOWZ3c+Tyt3/i4/pglyZctG3J4V1hWQ==
|
||||
dependencies:
|
||||
core-js "^2.5.7"
|
||||
joi "^13.0.0"
|
||||
"@hapi/joi" "^15.0.3"
|
||||
core-js "^2.6.5"
|
||||
minimist "^1.2.0"
|
||||
request "^2.88.0"
|
||||
rx "^4.1.0"
|
||||
|
||||
@ -16,7 +16,7 @@ The following features will be implemented. This gets done in three steps:
|
||||
|
||||
### User Account
|
||||
|
||||
[Cucumber Features](./integration/user_account)
|
||||
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/user_account)
|
||||
|
||||
* Sign-up
|
||||
* Agree to Data Privacy Statement
|
||||
@ -34,7 +34,7 @@ The following features will be implemented. This gets done in three steps:
|
||||
|
||||
### User Profile
|
||||
|
||||
[Cucumber Features](./integration/user_profile)
|
||||
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/user_profile)
|
||||
|
||||
* Upload and Change Avatar
|
||||
* Upload and Change Profile Picture
|
||||
@ -59,7 +59,7 @@ The following features will be implemented. This gets done in three steps:
|
||||
|
||||
### Posts
|
||||
|
||||
[Cucumber Features](./integration/post/)
|
||||
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/post)
|
||||
|
||||
* Creating Posts
|
||||
* Persistent Links
|
||||
@ -78,13 +78,13 @@ The following features will be implemented. This gets done in three steps:
|
||||
|
||||
### Comments
|
||||
|
||||
* Creating Comments
|
||||
* Creating Comments
|
||||
* Deleting Comments
|
||||
* Editing Comments
|
||||
* Upvote comments of others
|
||||
|
||||
### Notifications
|
||||
[Cucumber features](./integration/notifications)
|
||||
[Cucumber features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/notifications)
|
||||
|
||||
* User @-mentionings
|
||||
* Notify authors for comments
|
||||
@ -94,12 +94,12 @@ The following features will be implemented. This gets done in three steps:
|
||||
|
||||
* Show Posts by Tiles
|
||||
* Show Posts as List
|
||||
* Filter by Category \(Health and Wellbeing, Global Peace & Non-Violence, ...\)
|
||||
* Filter by Category \(Health and Wellbeing, Global Peace & Non-Violence, ...\)
|
||||
* Filter by Mood \(Funny, Happy, Surprised, Cry, Angry, ...\)
|
||||
* Filter by Source \(Connections, Following, Individuals, Non-Profits, ...\)
|
||||
* Filter by Posts & Tools \(Post, Events, CanDos, ...\)
|
||||
* Filter by Format Type \(Text, Pictures, Video, ...\)
|
||||
* Extended Filter \(Continent, Country, Language, ...\)
|
||||
* Extended Filter \(Continent, Country, Language, ...\)
|
||||
* Sort Posts by Date
|
||||
* Sort Posts by Shouts
|
||||
* Sort Posts by most Comments
|
||||
@ -116,7 +116,7 @@ The following features will be implemented. This gets done in three steps:
|
||||
|
||||
### Search
|
||||
|
||||
[Cucumber Features](./integration/search)
|
||||
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/search)
|
||||
|
||||
* Search for Categories
|
||||
* Search for Tags
|
||||
@ -186,13 +186,13 @@ The following features will be implemented. This gets done in three steps:
|
||||
|
||||
### More Info
|
||||
|
||||
Shows autmatically releated information for existing post.
|
||||
Shows automatically related information for existing post.
|
||||
|
||||
* Show related Posts
|
||||
* Show Pros and Cons
|
||||
* Show Bestlist
|
||||
* Show Votes
|
||||
* Link to corresponding Chatroom
|
||||
* Link to corresponding Chatroom
|
||||
|
||||
### Take Action
|
||||
|
||||
@ -237,7 +237,7 @@ Shows automatically related actions for existing post.
|
||||
|
||||
### Moderation
|
||||
|
||||
[Cucumber Features](./integration/moderation)
|
||||
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/moderation)
|
||||
|
||||
* Report Button for users for doubtful Content
|
||||
* Moderator Panel
|
||||
@ -262,7 +262,7 @@ Shows automatically related actions for existing post.
|
||||
|
||||
### Internationalization
|
||||
|
||||
[Cucumber Features](./integration/internationalization)
|
||||
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/internationalization)
|
||||
|
||||
* Frontend UI
|
||||
* Backend Error Messages
|
||||
@ -276,4 +276,3 @@ Shows automatically related actions for existing post.
|
||||
* Receiving Undo and Delete Activities for Articles and Notes
|
||||
* Serving Webfinger records and Actor Objects
|
||||
* Serving Followers, Following and Outbox collections
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Given, When, Then } from "cypress-cucumber-preprocessor/steps";
|
||||
import { getLangByName } from "../../support/helpers";
|
||||
import slugify from 'slug'
|
||||
|
||||
/* global cy */
|
||||
|
||||
@ -11,6 +12,7 @@ let loginCredentials = {
|
||||
};
|
||||
const narratorParams = {
|
||||
name: "Peter Pan",
|
||||
slug: 'peter-pan',
|
||||
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
|
||||
...loginCredentials
|
||||
};
|
||||
@ -171,10 +173,11 @@ When("I press {string}", label => {
|
||||
});
|
||||
|
||||
Given("we have the following posts in our database:", table => {
|
||||
table.hashes().forEach(({ Author, ...postAttributes }) => {
|
||||
table.hashes().forEach(({ Author, ...postAttributes }, i) => {
|
||||
Author = Author || `author-${i}`
|
||||
const userAttributes = {
|
||||
name: Author,
|
||||
email: `${Author}@example.org`,
|
||||
email: `${slugify(Author, {lower: true})}@example.org`,
|
||||
password: "1234"
|
||||
};
|
||||
postAttributes.deleted = Boolean(postAttributes.deleted);
|
||||
@ -273,9 +276,9 @@ When("I fill the password form with:", table => {
|
||||
table = table.rowsHash();
|
||||
cy.get("input[id=oldPassword]")
|
||||
.type(table["Your old password"])
|
||||
.get("input[id=newPassword]")
|
||||
.get("input[id=password]")
|
||||
.type(table["Your new passsword"])
|
||||
.get("input[id=confirmPassword]")
|
||||
.get("input[id=passwordConfirmation]")
|
||||
.type(table["Confirm new password"]);
|
||||
});
|
||||
|
||||
|
||||
@ -6,9 +6,9 @@ Feature: Search
|
||||
Background:
|
||||
Given I have a user account
|
||||
And we have the following posts in our database:
|
||||
| Author | id | title | content |
|
||||
| Brianna Wiest | p1 | 101 Essays that will change the way you think | 101 Essays, of course! |
|
||||
| Brianna Wiest | p2 | No searched for content | will be found in this post, I guarantee |
|
||||
| id | title | content |
|
||||
| p1 | 101 Essays that will change the way you think | 101 Essays, of course! |
|
||||
| p2 | No searched for content | will be found in this post, I guarantee |
|
||||
Given I am logged in
|
||||
|
||||
Scenario: Search for specific words
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import Factory from '../../backend/src/seed/factories'
|
||||
import { getDriver } from '../../backend/src/bootstrap/neo4j'
|
||||
import setupNeode from '../../backend/src/bootstrap/neode'
|
||||
import neode from 'neode'
|
||||
|
||||
const neo4jDriver = getDriver({
|
||||
const neo4jConfigs = {
|
||||
uri: Cypress.env('NEO4J_URI'),
|
||||
username: Cypress.env('NEO4J_USERNAME'),
|
||||
password: Cypress.env('NEO4J_PASSWORD')
|
||||
})
|
||||
const factory = Factory({ neo4jDriver })
|
||||
}
|
||||
const neo4jDriver = getDriver(neo4jConfigs)
|
||||
const factory = Factory({ seedServerHost, neo4jDriver, neodeInstance: setupNeode(neo4jConfigs)})
|
||||
const seedServerHost = Cypress.env('SEED_SERVER_HOST')
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -14,30 +17,33 @@ beforeEach(async () => {
|
||||
})
|
||||
|
||||
Cypress.Commands.add('factory', () => {
|
||||
return Factory({ seedServerHost })
|
||||
return Factory({ seedServerHost, neo4jDriver, neodeInstance: setupNeode(neo4jConfigs) })
|
||||
})
|
||||
|
||||
Cypress.Commands.add(
|
||||
'create',
|
||||
{ prevSubject: true },
|
||||
(factory, node, properties) => {
|
||||
return factory.create(node, properties)
|
||||
async (factory, node, properties) => {
|
||||
await factory.create(node, properties)
|
||||
return factory
|
||||
}
|
||||
)
|
||||
|
||||
Cypress.Commands.add(
|
||||
'relate',
|
||||
{ prevSubject: true },
|
||||
(factory, node, relationship, properties) => {
|
||||
return factory.relate(node, relationship, properties)
|
||||
async (factory, node, relationship, properties) => {
|
||||
await factory.relate(node, relationship, properties)
|
||||
return factory
|
||||
}
|
||||
)
|
||||
|
||||
Cypress.Commands.add(
|
||||
'mutate',
|
||||
{ prevSubject: true },
|
||||
(factory, mutation, variables) => {
|
||||
return factory.mutate(mutation, variables)
|
||||
async (factory, mutation, variables) => {
|
||||
await factory.mutate(mutation, variables)
|
||||
return factory
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -32,21 +32,9 @@
|
||||
value: 1G
|
||||
- name: NEO4J_dbms_memory_heap_max__size
|
||||
value: 1G
|
||||
- name: NEO4J_URI
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: configmap
|
||||
key: NEO4J_URI
|
||||
- name: NEO4J_USER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: configmap
|
||||
key: NEO4J_USER
|
||||
- name: NEO4J_AUTH
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: configmap
|
||||
key: NEO4J_AUTH
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: configmap
|
||||
ports:
|
||||
- containerPort: 7687
|
||||
- containerPort: 7474
|
||||
|
||||
@ -10,7 +10,8 @@
|
||||
GRAPHQL_URI: "http://nitro-backend.human-connection:4000"
|
||||
MOCKS: "false"
|
||||
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
|
||||
NEO4J_USER: "neo4j"
|
||||
NEO4J_USERNAME: "neo4j"
|
||||
NEO4J_PASSWORD: "neo4j"
|
||||
NEO4J_AUTH: "none"
|
||||
CLIENT_URI: "https://nitro-staging.human-connection.org"
|
||||
metadata:
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
[?] type: String, // in nitro this is a defined enum - seems good for now
|
||||
[X] required: true
|
||||
},
|
||||
[X] key: {
|
||||
[X] id: {
|
||||
[X] type: String,
|
||||
[X] required: true
|
||||
},
|
||||
@ -43,7 +43,7 @@
|
||||
CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as badge
|
||||
MERGE(b:Badge {id: badge._id["$oid"]})
|
||||
ON CREATE SET
|
||||
b.key = badge.key,
|
||||
b.id = badge.key,
|
||||
b.type = badge.type,
|
||||
b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''),
|
||||
b.status = badge.status,
|
||||
|
||||
@ -0,0 +1 @@
|
||||
MATCH (u:User)-[e:EMOTED]->(c:Post) DETACH DELETE e;
|
||||
@ -5,31 +5,54 @@
|
||||
// [-] Omitted in Nitro
|
||||
// [?] Unclear / has work to be done for Nitro
|
||||
{
|
||||
[ ] userId: {
|
||||
[ ] type: String,
|
||||
[ ] required: true,
|
||||
[X] userId: {
|
||||
[X] type: String,
|
||||
[X] required: true,
|
||||
[-] index: true
|
||||
},
|
||||
[ ] contributionId: {
|
||||
[ ] type: String,
|
||||
[ ] required: true,
|
||||
[X] contributionId: {
|
||||
[X] type: String,
|
||||
[X] required: true,
|
||||
[-] index: true
|
||||
},
|
||||
[ ] rated: {
|
||||
[ ] type: String,
|
||||
[?] rated: {
|
||||
[X] type: String,
|
||||
[ ] required: true,
|
||||
[ ] enum: ['funny', 'happy', 'surprised', 'cry', 'angry']
|
||||
[?] enum: ['funny', 'happy', 'surprised', 'cry', 'angry']
|
||||
},
|
||||
[ ] createdAt: {
|
||||
[ ] type: Date,
|
||||
[ ] default: Date.now
|
||||
[X] createdAt: {
|
||||
[X] type: Date,
|
||||
[X] default: Date.now
|
||||
},
|
||||
[ ] updatedAt: {
|
||||
[ ] type: Date,
|
||||
[ ] default: Date.now
|
||||
[X] updatedAt: {
|
||||
[X] type: Date,
|
||||
[X] default: Date.now
|
||||
},
|
||||
[ ] wasSeeded: { type: Boolean }
|
||||
[-] wasSeeded: { type: Boolean }
|
||||
}
|
||||
*/
|
||||
|
||||
CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as emotion;
|
||||
CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as emotion
|
||||
MATCH (u:User {id: emotion.userId}),
|
||||
(c:Post {id: emotion.contributionId})
|
||||
MERGE (u)-[e:EMOTED {
|
||||
id: emotion._id["$oid"],
|
||||
emotion: emotion.rated,
|
||||
createdAt: datetime(emotion.createdAt.`$date`),
|
||||
updatedAt: datetime(emotion.updatedAt.`$date`)
|
||||
}]->(c)
|
||||
RETURN e;
|
||||
/*
|
||||
// Queries
|
||||
// user sets an emotion emotion:
|
||||
// MERGE (u)-[e:EMOTED {id: ..., emotion: "funny", createdAt: ..., updatedAt: ...}]->(c)
|
||||
// user removes emotion
|
||||
// MATCH (u)-[e:EMOTED]->(c) DELETE e
|
||||
// contribution distributions over every `emotion` property value for one post
|
||||
// MATCH (u:User)-[e:EMOTED]->(c:Post {id: "5a70bbc8508f5b000b443b1a"}) RETURN e.emotion,COUNT(e.emotion)
|
||||
// contribution distributions over every `emotion` property value for one user (advanced - "whats the primary emotion used by the user?")
|
||||
// MATCH (u:User{id:"5a663b1ac64291000bf302a1"})-[e:EMOTED]->(c:Post) RETURN e.emotion,COUNT(e.emotion)
|
||||
// contribution distributions over every `emotion` property value for all posts created by one user (advanced - "how do others react to my contributions?")
|
||||
// MATCH (u:User)-[e:EMOTED]->(c:Post)<-[w:WROTE]-(a:User{id:"5a663b1ac64291000bf302a1"}) RETURN e.emotion,COUNT(e.emotion)
|
||||
// if we can filter the above an a variable timescale that would be great (should be possible on createdAt and updatedAt fields)
|
||||
*/
|
||||
@ -1 +1 @@
|
||||
// this is just a relation between users(?) - no need to delete
|
||||
MATCH (u1:User)-[f:FOLLOWS]->(u2:User) DETACH DELETE f;
|
||||
@ -60,8 +60,8 @@ delete_collection "contributions" "contributions_post"
|
||||
delete_collection "contributions" "contributions_cando"
|
||||
delete_collection "shouts" "shouts"
|
||||
delete_collection "comments" "comments"
|
||||
delete_collection "emotions" "emotions"
|
||||
|
||||
#delete_collection "emotions"
|
||||
#delete_collection "invites"
|
||||
#delete_collection "notifications"
|
||||
#delete_collection "organizations"
|
||||
@ -82,12 +82,12 @@ import_collection "users" "users/users.cql"
|
||||
import_collection "follows_users" "follows/follows.cql"
|
||||
#import_collection "follows_organizations" "follows/follows.cql"
|
||||
import_collection "contributions_post" "contributions/contributions.cql"
|
||||
import_collection "contributions_cando" "contributions/contributions.cql"
|
||||
#import_collection "contributions_cando" "contributions/contributions.cql"
|
||||
#import_collection "contributions_DELETED" "contributions/contributions.cql"
|
||||
import_collection "shouts" "shouts/shouts.cql"
|
||||
import_collection "comments" "comments/comments.cql"
|
||||
import_collection "emotions" "emotions/emotions.cql"
|
||||
|
||||
# import_collection "emotions"
|
||||
# import_collection "invites"
|
||||
# import_collection "notifications"
|
||||
# import_collection "organizations"
|
||||
|
||||
@ -101,7 +101,7 @@ ON CREATE SET
|
||||
u.name = user.name,
|
||||
u.slug = user.slug,
|
||||
u.email = user.email,
|
||||
u.password = user.password,
|
||||
u.encryptedPassword = user.password,
|
||||
u.avatar = replace(user.avatar, 'https://api-alpha.human-connection.org', ''),
|
||||
u.coverImg = replace(user.coverImg, 'https://api-alpha.human-connection.org', ''),
|
||||
u.wasInvited = user.wasInvited,
|
||||
|
||||
@ -26,9 +26,4 @@ services:
|
||||
ports:
|
||||
- 4001:4001
|
||||
- 4123:4123
|
||||
neo4j:
|
||||
environment:
|
||||
- NEO4J_AUTH=none
|
||||
ports:
|
||||
- 7687:7687
|
||||
- 7474:7474
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ services:
|
||||
networks:
|
||||
- hc-network
|
||||
environment:
|
||||
- NUXT_BUILD=.nuxt-dist
|
||||
- HOST=0.0.0.0
|
||||
- GRAPHQL_URI=http://backend:4000
|
||||
- MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.bZ8KK9l70omjXbEkkbHGsQ"
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
# Edit this Documentation
|
||||
|
||||
Go to the section and theme you want to change: On the left navigator.
|
||||
|
||||
Click **Edit on GitHub** on the right.
|
||||
|
||||
On the **Issue** tab you’ll find the open issues. Read what need to be done by clicking on the issue you like to fix.
|
||||
|
||||
By going backwards in the browser **\(!\)**, again go to the **Code** tab.
|
||||
Find the [**table of contents** for this documentation on GitHub](https://github.com/Human-Connection/Human-Connection/blob/master/SUMMARY.md) and navigate to the file you need to update.
|
||||
|
||||
Click on the **edit pencil** on the right side directly above the text to edit this file on your fork of Human Connection \(HC\).
|
||||
|
||||
@ -14,7 +8,7 @@ You can see a preview of your changes by clicking the **Preview changes** tab as
|
||||
|
||||
If you are ready, fill in the **Propose file change** at the end of the webpage.
|
||||
|
||||
After that you have to send your change to the HC basis with a pull request. Here make a comment which issue you have fixed. At least the number.
|
||||
After that you have to send your change to the HC basis with a pull request. Here make a comment which issue you have fixed. (If you are working on one of our [open issues](https://github.com/Human-Connection/Human-Connection/issues) please include the number.)
|
||||
|
||||
## Markdown your documentation
|
||||
|
||||
@ -117,4 +111,3 @@ TODO: How to modify screenshots in Linux ...
|
||||
{% endhint %}
|
||||
{% endtab %}
|
||||
{% endtabs %}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ for an interactive cypher shell and a visualization of the graph.
|
||||
|
||||
## Installation without Docker
|
||||
|
||||
Install community edition of [Neo4J]() along with the plugin
|
||||
Install the community edition of [Neo4j](https://neo4j.com/) 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
|
||||
@ -28,7 +28,13 @@ To do so, go to [releases](https://neo4j.com/download-center/#releases), choose
|
||||
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.
|
||||
and drop the `.jar` file into the `plugins` folder of the just extracted Neo4j-Server.
|
||||
|
||||
Then make sure to allow Apoc procedures by adding the following line to your Neo4j configuration \(`conf/neo4j.conf`\):
|
||||
|
||||
```
|
||||
dbms.security.procedures.unrestricted=apoc.*
|
||||
```
|
||||
|
||||
### Alternatives
|
||||
|
||||
@ -59,6 +65,6 @@ $ 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).
|
||||
Otherwise, if you don't have `cypher-shell` available, copy the cypher
|
||||
statements [from the `db_setup.sh` script](https://github.com/Human-Connection/Human-Connection/blob/master/neo4j/db_setup.sh) and paste the scripts into your
|
||||
[database browser frontend](http://localhost:7474).
|
||||
|
||||
@ -19,16 +19,19 @@
|
||||
"test:jest": "cd webapp && yarn test && cd ../backend && yarn test:jest && codecov"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"codecov": "^3.5.0",
|
||||
"cross-env": "^5.2.0",
|
||||
"cypress": "^3.3.1",
|
||||
"cypress": "^3.4.0",
|
||||
"cypress-cucumber-preprocessor": "^1.12.0",
|
||||
"cypress-file-upload": "^3.1.4",
|
||||
"cypress-file-upload": "^3.3.1",
|
||||
"cypress-plugin-retries": "^1.2.2",
|
||||
"dotenv": "^8.0.0",
|
||||
"faker": "Marak/faker.js#master",
|
||||
"graphql-request": "^1.8.2",
|
||||
"neo4j-driver": "^1.7.5",
|
||||
"npm-run-all": "^4.1.5"
|
||||
"neode": "^0.2.16",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"slug": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
2
webapp/.gitignore
vendored
2
webapp/.gitignore
vendored
@ -61,6 +61,8 @@ typings/
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
# also the build output in docker container
|
||||
.nuxt-dist
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM node:12.4-alpine as base
|
||||
FROM node:12.6-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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user