mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge branch 'master' of https://github.com/Human-Connection/Human-Connection into 1395-hashtags-imported-with-not-allowed-chars
This commit is contained in:
commit
0037fc90de
@ -19,9 +19,8 @@
|
|||||||
"test:jest": "run-p --race test:before:* \"test:jest:cmd {@}\" --",
|
"test:jest": "run-p --race test:before:* \"test:jest:cmd {@}\" --",
|
||||||
"test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --",
|
"test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --",
|
||||||
"test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --",
|
"test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --",
|
||||||
"db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js",
|
"db:reset": "babel-node src/seed/reset-db.js",
|
||||||
"db:reset": "cross-env babel-node src/seed/reset-db.js",
|
"db:seed": "babel-node src/seed/seed-db.js"
|
||||||
"db:seed": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions run-p --race dev db:script:seed"
|
|
||||||
},
|
},
|
||||||
"author": "Human Connection gGmbH",
|
"author": "Human Connection gGmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -49,19 +48,19 @@
|
|||||||
"apollo-client": "~2.6.4",
|
"apollo-client": "~2.6.4",
|
||||||
"apollo-link-context": "~1.0.18",
|
"apollo-link-context": "~1.0.18",
|
||||||
"apollo-link-http": "~1.5.15",
|
"apollo-link-http": "~1.5.15",
|
||||||
"apollo-server": "~2.9.1",
|
"apollo-server": "~2.9.3",
|
||||||
"apollo-server-express": "^2.9.0",
|
"apollo-server-express": "^2.9.0",
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"cheerio": "~1.0.0-rc.3",
|
"cheerio": "~1.0.0-rc.3",
|
||||||
"cors": "~2.8.5",
|
"cors": "~2.8.5",
|
||||||
"cross-env": "~5.2.0",
|
"cross-env": "~5.2.1",
|
||||||
"date-fns": "2.0.1",
|
"date-fns": "2.0.1",
|
||||||
"debug": "~4.1.1",
|
"debug": "~4.1.1",
|
||||||
"dotenv": "~8.1.0",
|
"dotenv": "~8.1.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"faker": "Marak/faker.js#master",
|
"faker": "Marak/faker.js#master",
|
||||||
"graphql": "^14.5.3",
|
"graphql": "^14.5.4",
|
||||||
"graphql-custom-directives": "~0.2.14",
|
"graphql-custom-directives": "~0.2.14",
|
||||||
"graphql-iso-date": "~3.6.1",
|
"graphql-iso-date": "~3.6.1",
|
||||||
"graphql-middleware": "~3.0.5",
|
"graphql-middleware": "~3.0.5",
|
||||||
@ -90,7 +89,7 @@
|
|||||||
"metascraper-video": "^5.6.5",
|
"metascraper-video": "^5.6.5",
|
||||||
"metascraper-youtube": "^5.6.3",
|
"metascraper-youtube": "^5.6.3",
|
||||||
"minimatch": "^3.0.4",
|
"minimatch": "^3.0.4",
|
||||||
"neo4j-driver": "~1.7.5",
|
"neo4j-driver": "~1.7.6",
|
||||||
"neo4j-graphql-js": "^2.7.2",
|
"neo4j-graphql-js": "^2.7.2",
|
||||||
"neode": "^0.3.2",
|
"neode": "^0.3.2",
|
||||||
"node-fetch": "~2.6.0",
|
"node-fetch": "~2.6.0",
|
||||||
@ -111,18 +110,18 @@
|
|||||||
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
||||||
"@babel/preset-env": "~7.5.5",
|
"@babel/preset-env": "~7.5.5",
|
||||||
"@babel/register": "~7.5.5",
|
"@babel/register": "~7.5.5",
|
||||||
"apollo-server-testing": "~2.9.1",
|
"apollo-server-testing": "~2.9.3",
|
||||||
"babel-core": "~7.0.0-0",
|
"babel-core": "~7.0.0-0",
|
||||||
"babel-eslint": "~10.0.3",
|
"babel-eslint": "~10.0.3",
|
||||||
"babel-jest": "~24.9.0",
|
"babel-jest": "~24.9.0",
|
||||||
"chai": "~4.2.0",
|
"chai": "~4.2.0",
|
||||||
"cucumber": "~5.1.0",
|
"cucumber": "~5.1.0",
|
||||||
"eslint": "~6.2.2",
|
"eslint": "~6.3.0",
|
||||||
"eslint-config-prettier": "~6.1.0",
|
"eslint-config-prettier": "~6.1.0",
|
||||||
"eslint-config-standard": "~14.1.0",
|
"eslint-config-standard": "~14.1.0",
|
||||||
"eslint-plugin-import": "~2.18.2",
|
"eslint-plugin-import": "~2.18.2",
|
||||||
"eslint-plugin-jest": "~22.15.2",
|
"eslint-plugin-jest": "~22.16.0",
|
||||||
"eslint-plugin-node": "~9.1.0",
|
"eslint-plugin-node": "~9.2.0",
|
||||||
"eslint-plugin-prettier": "~3.1.0",
|
"eslint-plugin-prettier": "~3.1.0",
|
||||||
"eslint-plugin-promise": "~4.2.1",
|
"eslint-plugin-promise": "~4.2.1",
|
||||||
"eslint-plugin-standard": "~4.0.1",
|
"eslint-plugin-standard": "~4.0.1",
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export default async (driver, authorizationHeader) => {
|
|||||||
}
|
}
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
const query = `
|
const query = `
|
||||||
MATCH (user:User {id: {id} })
|
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
||||||
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`
|
`
|
||||||
@ -23,7 +23,6 @@ export default async (driver, authorizationHeader) => {
|
|||||||
return record.get('user')
|
return record.get('user')
|
||||||
})
|
})
|
||||||
if (!currentUser) return null
|
if (!currentUser) return null
|
||||||
if (currentUser.disabled) return null
|
|
||||||
return {
|
return {
|
||||||
token,
|
token,
|
||||||
...currentUser,
|
...currentUser,
|
||||||
|
|||||||
104
backend/src/jwt/decode.spec.js
Normal file
104
backend/src/jwt/decode.spec.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import Factory from '../seed/factories/index'
|
||||||
|
import { getDriver } from '../bootstrap/neo4j'
|
||||||
|
import decode from './decode'
|
||||||
|
|
||||||
|
const factory = Factory()
|
||||||
|
const driver = getDriver()
|
||||||
|
|
||||||
|
// here is the decoded JWT token:
|
||||||
|
// {
|
||||||
|
// role: 'user',
|
||||||
|
// locationName: null,
|
||||||
|
// name: 'Jenny Rostock',
|
||||||
|
// about: null,
|
||||||
|
// avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
|
||||||
|
// id: 'u3',
|
||||||
|
// email: 'user@example.org',
|
||||||
|
// slug: 'jenny-rostock',
|
||||||
|
// iat: 1550846680,
|
||||||
|
// exp: 1637246680,
|
||||||
|
// aud: 'http://localhost:3000',
|
||||||
|
// iss: 'http://localhost:4000',
|
||||||
|
// sub: 'u3'
|
||||||
|
// }
|
||||||
|
export const validAuthorizationHeader =
|
||||||
|
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc'
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('decode', () => {
|
||||||
|
let authorizationHeader
|
||||||
|
const returnsNull = async () => {
|
||||||
|
await expect(decode(driver, authorizationHeader)).resolves.toBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('given `null` as JWT Bearer token', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationHeader = null
|
||||||
|
})
|
||||||
|
it('returns null', returnsNull)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given no JWT Bearer token', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationHeader = undefined
|
||||||
|
})
|
||||||
|
it('returns null', returnsNull)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given malformed JWT Bearer token', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationHeader = 'blah'
|
||||||
|
})
|
||||||
|
it('returns null', returnsNull)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given valid JWT Bearer token', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationHeader = validAuthorizationHeader
|
||||||
|
})
|
||||||
|
it('returns null', returnsNull)
|
||||||
|
|
||||||
|
describe('and corresponding user in the database', () => {
|
||||||
|
let user
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await factory.create('User', {
|
||||||
|
role: 'user',
|
||||||
|
name: 'Jenny Rostock',
|
||||||
|
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
|
||||||
|
id: 'u3',
|
||||||
|
email: 'user@example.org',
|
||||||
|
slug: 'jenny-rostock',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns user object except email', async () => {
|
||||||
|
await expect(decode(driver, authorizationHeader)).resolves.toMatchObject({
|
||||||
|
role: 'user',
|
||||||
|
name: 'Jenny Rostock',
|
||||||
|
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
|
||||||
|
id: 'u3',
|
||||||
|
email: null,
|
||||||
|
slug: 'jenny-rostock',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('but user is deleted', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await user.update({ updatedAt: new Date().toISOString(), deleted: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null', returnsNull)
|
||||||
|
})
|
||||||
|
describe('but user is disabled', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await user.update({ updatedAt: new Date().toISOString(), disabled: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null', returnsNull)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -12,11 +12,9 @@ export default {
|
|||||||
CreatePost: setCreatedAt,
|
CreatePost: setCreatedAt,
|
||||||
CreateComment: setCreatedAt,
|
CreateComment: setCreatedAt,
|
||||||
CreateOrganization: setCreatedAt,
|
CreateOrganization: setCreatedAt,
|
||||||
CreateNotification: setCreatedAt,
|
|
||||||
UpdateUser: setUpdatedAt,
|
UpdateUser: setUpdatedAt,
|
||||||
UpdatePost: setUpdatedAt,
|
UpdatePost: setUpdatedAt,
|
||||||
UpdateComment: setUpdatedAt,
|
UpdateComment: setUpdatedAt,
|
||||||
UpdateOrganization: setUpdatedAt,
|
UpdateOrganization: setUpdatedAt,
|
||||||
UpdateNotification: setUpdatedAt,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
import { gql } from '../../jest/helpers'
|
||||||
import { host, login } from '../../jest/helpers'
|
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import { neode } from '../../bootstrap/neo4j'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import createServer from '../../server'
|
||||||
|
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
const instance = neode()
|
const neode = getNeode()
|
||||||
|
const driver = getDriver()
|
||||||
|
|
||||||
|
let authenticatedUser
|
||||||
|
let user
|
||||||
|
let query
|
||||||
|
|
||||||
const currentUserParams = {
|
const currentUserParams = {
|
||||||
id: 'u1',
|
id: 'u1',
|
||||||
@ -26,24 +32,42 @@ const randomAuthorParams = {
|
|||||||
const categoryIds = ['cat9']
|
const categoryIds = ['cat9']
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await Promise.all([
|
const [currentUser, followedAuthor, randomAuthor] = await Promise.all([
|
||||||
factory.create('User', currentUserParams),
|
factory.create('User', currentUserParams),
|
||||||
factory.create('User', followedAuthorParams),
|
factory.create('User', followedAuthorParams),
|
||||||
factory.create('User', randomAuthorParams),
|
factory.create('User', randomAuthorParams),
|
||||||
])
|
])
|
||||||
await instance.create('Category', {
|
user = currentUser
|
||||||
|
await neode.create('Category', {
|
||||||
id: 'cat9',
|
id: 'cat9',
|
||||||
name: 'Democracy & Politics',
|
name: 'Democracy & Politics',
|
||||||
icon: 'university',
|
icon: 'university',
|
||||||
})
|
})
|
||||||
const [asYourself, asFollowedUser, asSomeoneElse] = await Promise.all([
|
await currentUser.relateTo(followedAuthor, 'following')
|
||||||
Factory().authenticateAs(currentUserParams),
|
await factory.create('Post', {
|
||||||
Factory().authenticateAs(followedAuthorParams),
|
author: followedAuthor,
|
||||||
Factory().authenticateAs(randomAuthorParams),
|
title: 'This is the post of a followed user',
|
||||||
])
|
categoryIds,
|
||||||
await asYourself.follow({ id: 'u2', type: 'User' })
|
})
|
||||||
await asFollowedUser.create('Post', { title: 'This is the post of a followed user', categoryIds })
|
await factory.create('Post', {
|
||||||
await asSomeoneElse.create('Post', { title: 'This is some random post', categoryIds })
|
author: randomAuthor,
|
||||||
|
title: 'This is some random post',
|
||||||
|
categoryIds,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
driver,
|
||||||
|
neode,
|
||||||
|
user: authenticatedUser,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const client = createTestClient(server)
|
||||||
|
query = client.query
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@ -52,33 +76,44 @@ afterEach(async () => {
|
|||||||
|
|
||||||
describe('Filter posts by author is followed by sb.', () => {
|
describe('Filter posts by author is followed by sb.', () => {
|
||||||
describe('given an authenticated user', () => {
|
describe('given an authenticated user', () => {
|
||||||
let authenticatedClient
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login(currentUserParams)
|
authenticatedUser = await user.toJson()
|
||||||
authenticatedClient = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('no filter bubble', () => {
|
describe('no filter bubble', () => {
|
||||||
it('returns all posts', async () => {
|
it('returns all posts', async () => {
|
||||||
const query = '{ Post(filter: { }) { title } }'
|
const postQuery = gql`
|
||||||
|
{
|
||||||
|
Post(filter: {}) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
const expected = {
|
const expected = {
|
||||||
Post: [
|
data: {
|
||||||
{ title: 'This is some random post' },
|
Post: [
|
||||||
{ title: 'This is the post of a followed user' },
|
{ title: 'This is some random post' },
|
||||||
],
|
{ title: 'This is the post of a followed user' },
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await expect(authenticatedClient.request(query)).resolves.toEqual(expected)
|
await expect(query({ query: postQuery })).resolves.toMatchObject(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('filtering for posts of followed users only', () => {
|
describe('filtering for posts of followed users only', () => {
|
||||||
it('returns only posts authored by followed users', async () => {
|
it('returns only posts authored by followed users', async () => {
|
||||||
const query = '{ Post( filter: { author: { followedBy_some: { id: "u1" } } }) { title } }'
|
const postQuery = gql`
|
||||||
|
{
|
||||||
|
Post(filter: { author: { followedBy_some: { id: "u1" } } }) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
const expected = {
|
const expected = {
|
||||||
Post: [{ title: 'This is the post of a followed user' }],
|
data: { Post: [{ title: 'This is the post of a followed user' }] },
|
||||||
}
|
}
|
||||||
await expect(authenticatedClient.request(query)).resolves.toEqual(expected)
|
await expect(query({ query: postQuery })).resolves.toMatchObject(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,13 +4,13 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
|||||||
if (!idsOfUsers.length) return
|
if (!idsOfUsers.length) return
|
||||||
|
|
||||||
// Checked here, because it does not go through GraphQL checks at all in this file.
|
// Checked here, because it does not go through GraphQL checks at all in this file.
|
||||||
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_post']
|
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post']
|
||||||
if (!reasonsAllowed.includes(reason)) {
|
if (!reasonsAllowed.includes(reason)) {
|
||||||
throw new Error('Notification reason is not allowed!')
|
throw new Error('Notification reason is not allowed!')
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(label === 'Post' && reason !== 'mentioned_in_post') ||
|
(label === 'Post' && reason !== 'mentioned_in_post') ||
|
||||||
(label === 'Comment' && !['mentioned_in_comment', 'comment_on_post'].includes(reason))
|
(label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
|
||||||
) {
|
) {
|
||||||
throw new Error('Notification does not fit the reason!')
|
throw new Error('Notification does not fit the reason!')
|
||||||
}
|
}
|
||||||
@ -25,8 +25,9 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
|||||||
MATCH (user: User)
|
MATCH (user: User)
|
||||||
WHERE user.id in $idsOfUsers
|
WHERE user.id in $idsOfUsers
|
||||||
AND NOT (user)<-[:BLOCKED]-(author)
|
AND NOT (user)<-[:BLOCKED]-(author)
|
||||||
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt })
|
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||||
MERGE (post)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
|
SET notification.read = FALSE
|
||||||
|
SET notification.createdAt = $createdAt
|
||||||
`
|
`
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -37,20 +38,22 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
|||||||
WHERE user.id in $idsOfUsers
|
WHERE user.id in $idsOfUsers
|
||||||
AND NOT (user)<-[:BLOCKED]-(author)
|
AND NOT (user)<-[:BLOCKED]-(author)
|
||||||
AND NOT (user)<-[:BLOCKED]-(postAuthor)
|
AND NOT (user)<-[:BLOCKED]-(postAuthor)
|
||||||
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt })
|
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||||
MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
|
SET notification.read = FALSE
|
||||||
|
SET notification.createdAt = $createdAt
|
||||||
`
|
`
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'comment_on_post': {
|
case 'commented_on_post': {
|
||||||
cypher = `
|
cypher = `
|
||||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||||
MATCH (user: User)
|
MATCH (user: User)
|
||||||
WHERE user.id in $idsOfUsers
|
WHERE user.id in $idsOfUsers
|
||||||
AND NOT (user)<-[:BLOCKED]-(author)
|
AND NOT (user)<-[:BLOCKED]-(author)
|
||||||
AND NOT (author)<-[:BLOCKED]-(user)
|
AND NOT (author)<-[:BLOCKED]-(user)
|
||||||
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt })
|
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||||
MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
|
SET notification.read = FALSE
|
||||||
|
SET notification.createdAt = $createdAt
|
||||||
`
|
`
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -105,7 +108,7 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) =>
|
|||||||
return record.get('user')
|
return record.get('user')
|
||||||
})
|
})
|
||||||
if (context.user.id !== postAuthor.id) {
|
if (context.user.id !== postAuthor.id) {
|
||||||
await notifyUsers('Comment', comment.id, [postAuthor.id], 'comment_on_post', context)
|
await notifyUsers('Comment', comment.id, [postAuthor.id], 'commented_on_post', context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -77,14 +77,18 @@ afterEach(async () => {
|
|||||||
describe('notifications', () => {
|
describe('notifications', () => {
|
||||||
const notificationQuery = gql`
|
const notificationQuery = gql`
|
||||||
query($read: Boolean) {
|
query($read: Boolean) {
|
||||||
currentUser {
|
notifications(read: $read, orderBy: createdAt_desc) {
|
||||||
notifications(read: $read, orderBy: createdAt_desc) {
|
read
|
||||||
read
|
reason
|
||||||
reason
|
createdAt
|
||||||
post {
|
from {
|
||||||
|
__typename
|
||||||
|
... on Post {
|
||||||
|
id
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
comment {
|
... on Comment {
|
||||||
|
id
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,18 +158,18 @@ describe('notifications', () => {
|
|||||||
await createCommentOnPostAction()
|
await createCommentOnPostAction()
|
||||||
const expected = expect.objectContaining({
|
const expected = expect.objectContaining({
|
||||||
data: {
|
data: {
|
||||||
currentUser: {
|
notifications: [
|
||||||
notifications: [
|
{
|
||||||
{
|
read: false,
|
||||||
read: false,
|
createdAt: expect.any(String),
|
||||||
reason: 'comment_on_post',
|
reason: 'commented_on_post',
|
||||||
post: null,
|
from: {
|
||||||
comment: {
|
__typename: 'Comment',
|
||||||
content: commentContent,
|
id: 'c47',
|
||||||
},
|
content: commentContent,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const { query } = createTestClient(server)
|
const { query } = createTestClient(server)
|
||||||
@ -183,11 +187,7 @@ describe('notifications', () => {
|
|||||||
await notifiedUser.relateTo(commentAuthor, 'blocked')
|
await notifiedUser.relateTo(commentAuthor, 'blocked')
|
||||||
await createCommentOnPostAction()
|
await createCommentOnPostAction()
|
||||||
const expected = expect.objectContaining({
|
const expected = expect.objectContaining({
|
||||||
data: {
|
data: { notifications: [] },
|
||||||
currentUser: {
|
|
||||||
notifications: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const { query } = createTestClient(server)
|
const { query } = createTestClient(server)
|
||||||
await expect(
|
await expect(
|
||||||
@ -211,11 +211,7 @@ describe('notifications', () => {
|
|||||||
await notifiedUser.relateTo(commentAuthor, 'blocked')
|
await notifiedUser.relateTo(commentAuthor, 'blocked')
|
||||||
await createCommentOnPostAction()
|
await createCommentOnPostAction()
|
||||||
const expected = expect.objectContaining({
|
const expected = expect.objectContaining({
|
||||||
data: {
|
data: { notifications: [] },
|
||||||
currentUser: {
|
|
||||||
notifications: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const { query } = createTestClient(server)
|
const { query } = createTestClient(server)
|
||||||
await expect(
|
await expect(
|
||||||
@ -253,18 +249,18 @@ describe('notifications', () => {
|
|||||||
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
|
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
|
||||||
const expected = expect.objectContaining({
|
const expected = expect.objectContaining({
|
||||||
data: {
|
data: {
|
||||||
currentUser: {
|
notifications: [
|
||||||
notifications: [
|
{
|
||||||
{
|
read: false,
|
||||||
read: false,
|
createdAt: expect.any(String),
|
||||||
reason: 'mentioned_in_post',
|
reason: 'mentioned_in_post',
|
||||||
post: {
|
from: {
|
||||||
content: expectedContent,
|
__typename: 'Post',
|
||||||
},
|
id: 'p47',
|
||||||
comment: null,
|
content: expectedContent,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const { query } = createTestClient(server)
|
const { query } = createTestClient(server)
|
||||||
@ -278,7 +274,7 @@ describe('notifications', () => {
|
|||||||
).resolves.toEqual(expected)
|
).resolves.toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('many times', () => {
|
describe('updates the post and mentions me again', () => {
|
||||||
const updatePostAction = async () => {
|
const updatePostAction = async () => {
|
||||||
const updatedContent = `
|
const updatedContent = `
|
||||||
One more mention to
|
One more mention to
|
||||||
@ -307,33 +303,25 @@ describe('notifications', () => {
|
|||||||
authenticatedUser = await notifiedUser.toJson()
|
authenticatedUser = await notifiedUser.toJson()
|
||||||
}
|
}
|
||||||
|
|
||||||
it('creates exactly one more notification', async () => {
|
it('creates no duplicate notification for the same resource', async () => {
|
||||||
|
const expectedUpdatedContent =
|
||||||
|
'<br>One more mention to<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again:<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>'
|
||||||
await createPostAction()
|
await createPostAction()
|
||||||
await updatePostAction()
|
await updatePostAction()
|
||||||
const expectedContent =
|
|
||||||
'<br>One more mention to<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again:<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>'
|
|
||||||
const expected = expect.objectContaining({
|
const expected = expect.objectContaining({
|
||||||
data: {
|
data: {
|
||||||
currentUser: {
|
notifications: [
|
||||||
notifications: [
|
{
|
||||||
{
|
read: false,
|
||||||
read: false,
|
createdAt: expect.any(String),
|
||||||
reason: 'mentioned_in_post',
|
reason: 'mentioned_in_post',
|
||||||
post: {
|
from: {
|
||||||
content: expectedContent,
|
__typename: 'Post',
|
||||||
},
|
id: 'p47',
|
||||||
comment: null,
|
content: expectedUpdatedContent,
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
read: false,
|
],
|
||||||
reason: 'mentioned_in_post',
|
|
||||||
post: {
|
|
||||||
content: expectedContent,
|
|
||||||
},
|
|
||||||
comment: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await expect(
|
await expect(
|
||||||
@ -345,6 +333,68 @@ describe('notifications', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(expected)
|
).resolves.toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('if the notification was marked as read earlier', () => {
|
||||||
|
const markAsReadAction = async () => {
|
||||||
|
const mutation = gql`
|
||||||
|
mutation($id: ID!) {
|
||||||
|
markAsRead(id: $id) {
|
||||||
|
read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
await mutate({ mutation, variables: { id: 'p47' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('but the next mention happens after the notification was marked as read', () => {
|
||||||
|
it('sets the `read` attribute to false again', async () => {
|
||||||
|
await createPostAction()
|
||||||
|
await markAsReadAction()
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
notifications: [{ read: readBefore }],
|
||||||
|
},
|
||||||
|
} = await query({
|
||||||
|
query: notificationQuery,
|
||||||
|
})
|
||||||
|
await updatePostAction()
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
notifications: [{ read: readAfter }],
|
||||||
|
},
|
||||||
|
} = await query({
|
||||||
|
query: notificationQuery,
|
||||||
|
})
|
||||||
|
expect(readBefore).toEqual(true)
|
||||||
|
expect(readAfter).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates the `createdAt` attribute', async () => {
|
||||||
|
await createPostAction()
|
||||||
|
await markAsReadAction()
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
notifications: [{ createdAt: createdAtBefore }],
|
||||||
|
},
|
||||||
|
} = await query({
|
||||||
|
query: notificationQuery,
|
||||||
|
})
|
||||||
|
await updatePostAction()
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
notifications: [{ createdAt: createdAtAfter }],
|
||||||
|
},
|
||||||
|
} = await query({
|
||||||
|
query: notificationQuery,
|
||||||
|
})
|
||||||
|
expect(createdAtBefore).toBeTruthy()
|
||||||
|
expect(Date.parse(createdAtBefore)).toEqual(expect.any(Number))
|
||||||
|
expect(createdAtAfter).toBeTruthy()
|
||||||
|
expect(Date.parse(createdAtAfter)).toEqual(expect.any(Number))
|
||||||
|
expect(createdAtBefore).not.toEqual(createdAtAfter)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('but the author of the post blocked me', () => {
|
describe('but the author of the post blocked me', () => {
|
||||||
@ -355,11 +405,7 @@ describe('notifications', () => {
|
|||||||
it('sends no notification', async () => {
|
it('sends no notification', async () => {
|
||||||
await createPostAction()
|
await createPostAction()
|
||||||
const expected = expect.objectContaining({
|
const expected = expect.objectContaining({
|
||||||
data: {
|
data: { notifications: [] },
|
||||||
currentUser: {
|
|
||||||
notifications: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const { query } = createTestClient(server)
|
const { query } = createTestClient(server)
|
||||||
await expect(
|
await expect(
|
||||||
@ -397,18 +443,18 @@ describe('notifications', () => {
|
|||||||
await createCommentOnPostAction()
|
await createCommentOnPostAction()
|
||||||
const expected = expect.objectContaining({
|
const expected = expect.objectContaining({
|
||||||
data: {
|
data: {
|
||||||
currentUser: {
|
notifications: [
|
||||||
notifications: [
|
{
|
||||||
{
|
read: false,
|
||||||
read: false,
|
createdAt: expect.any(String),
|
||||||
reason: 'mentioned_in_comment',
|
reason: 'mentioned_in_comment',
|
||||||
post: null,
|
from: {
|
||||||
comment: {
|
__typename: 'Comment',
|
||||||
content: commentContent,
|
id: 'c47',
|
||||||
},
|
content: commentContent,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const { query } = createTestClient(server)
|
const { query } = createTestClient(server)
|
||||||
@ -440,11 +486,7 @@ describe('notifications', () => {
|
|||||||
it('sends no notification', async () => {
|
it('sends no notification', async () => {
|
||||||
await createCommentOnPostAction()
|
await createCommentOnPostAction()
|
||||||
const expected = expect.objectContaining({
|
const expected = expect.objectContaining({
|
||||||
data: {
|
data: { notifications: [] },
|
||||||
currentUser: {
|
|
||||||
notifications: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const { query } = createTestClient(server)
|
const { query } = createTestClient(server)
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@ -41,32 +41,6 @@ const isMySocialMedia = rule({
|
|||||||
return socialMedia.ownedBy.node.id === user.id
|
return socialMedia.ownedBy.node.id === user.id
|
||||||
})
|
})
|
||||||
|
|
||||||
const belongsToMe = rule({
|
|
||||||
cache: 'no_cache',
|
|
||||||
})(async (_, args, context) => {
|
|
||||||
const {
|
|
||||||
driver,
|
|
||||||
user: { id: userId },
|
|
||||||
} = context
|
|
||||||
const { id: notificationId } = args
|
|
||||||
const session = driver.session()
|
|
||||||
const result = await session.run(
|
|
||||||
`
|
|
||||||
MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId})
|
|
||||||
RETURN n
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
notificationId,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const [notification] = result.records.map(record => {
|
|
||||||
return record.get('n')
|
|
||||||
})
|
|
||||||
session.close()
|
|
||||||
return Boolean(notification)
|
|
||||||
})
|
|
||||||
|
|
||||||
/* TODO: decide if we want to remove this check: the check
|
/* TODO: decide if we want to remove this check: the check
|
||||||
* `onlyEnabledContent` throws authorization errors only if you have
|
* `onlyEnabledContent` throws authorization errors only if you have
|
||||||
* arguments for `disabled` or `deleted` assuming these are filter
|
* arguments for `disabled` or `deleted` assuming these are filter
|
||||||
@ -117,13 +91,11 @@ const isAuthor = rule({
|
|||||||
resourceId,
|
resourceId,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
session.close()
|
||||||
const [author] = result.records.map(record => {
|
const [author] = result.records.map(record => {
|
||||||
return record.get('author')
|
return record.get('author')
|
||||||
})
|
})
|
||||||
const {
|
const authorId = author && author.properties && author.properties.id
|
||||||
properties: { id: authorId },
|
|
||||||
} = author
|
|
||||||
session.close()
|
|
||||||
return authorId === user.id
|
return authorId === user.id
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -149,7 +121,6 @@ const permissions = shield(
|
|||||||
Category: allow,
|
Category: allow,
|
||||||
Tag: allow,
|
Tag: allow,
|
||||||
Report: isModerator,
|
Report: isModerator,
|
||||||
Notification: isAdmin,
|
|
||||||
statistics: allow,
|
statistics: allow,
|
||||||
currentUser: allow,
|
currentUser: allow,
|
||||||
Post: or(onlyEnabledContent, isModerator),
|
Post: or(onlyEnabledContent, isModerator),
|
||||||
@ -158,8 +129,9 @@ const permissions = shield(
|
|||||||
isLoggedIn: allow,
|
isLoggedIn: allow,
|
||||||
Badge: allow,
|
Badge: allow,
|
||||||
PostsEmotionsCountByEmotion: allow,
|
PostsEmotionsCountByEmotion: allow,
|
||||||
PostsEmotionsByCurrentUser: allow,
|
PostsEmotionsByCurrentUser: isAuthenticated,
|
||||||
blockedUsers: isAuthenticated,
|
blockedUsers: isAuthenticated,
|
||||||
|
notifications: isAuthenticated,
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
'*': deny,
|
'*': deny,
|
||||||
@ -168,7 +140,6 @@ const permissions = shield(
|
|||||||
Signup: isAdmin,
|
Signup: isAdmin,
|
||||||
SignupVerification: allow,
|
SignupVerification: allow,
|
||||||
CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)),
|
CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)),
|
||||||
UpdateNotification: belongsToMe,
|
|
||||||
UpdateUser: onlyYourself,
|
UpdateUser: onlyYourself,
|
||||||
CreatePost: isAuthenticated,
|
CreatePost: isAuthenticated,
|
||||||
UpdatePost: isAuthor,
|
UpdatePost: isAuthor,
|
||||||
@ -198,6 +169,7 @@ const permissions = shield(
|
|||||||
RemovePostEmotions: isAuthenticated,
|
RemovePostEmotions: isAuthenticated,
|
||||||
block: isAuthenticated,
|
block: isAuthenticated,
|
||||||
unblock: isAuthenticated,
|
unblock: isAuthenticated,
|
||||||
|
markAsRead: isAuthenticated,
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: isMyOwn,
|
email: isMyOwn,
|
||||||
|
|||||||
@ -13,15 +13,16 @@ const setDefaultFilters = (resolve, root, args, context, info) => {
|
|||||||
return resolve(root, args, context, info)
|
return resolve(root, args, context, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
const obfuscateDisabled = async (resolve, root, args, context, info) => {
|
const obfuscate = async (resolve, root, args, context, info) => {
|
||||||
if (!isModerator(context) && root.disabled) {
|
if (root.deleted || (!isModerator(context) && root.disabled)) {
|
||||||
root.content = 'UNAVAILABLE'
|
root.content = 'UNAVAILABLE'
|
||||||
root.contentExcerpt = 'UNAVAILABLE'
|
root.contentExcerpt = 'UNAVAILABLE'
|
||||||
root.title = 'UNAVAILABLE'
|
root.title = 'UNAVAILABLE'
|
||||||
root.image = 'UNAVAILABLE'
|
root.slug = 'UNAVAILABLE'
|
||||||
root.avatar = 'UNAVAILABLE'
|
root.avatar = 'UNAVAILABLE'
|
||||||
root.about = 'UNAVAILABLE'
|
root.about = 'UNAVAILABLE'
|
||||||
root.name = 'UNAVAILABLE'
|
root.name = 'UNAVAILABLE'
|
||||||
|
root.image = null // avoid unecessary 500 errors
|
||||||
}
|
}
|
||||||
return resolve(root, args, context, info)
|
return resolve(root, args, context, info)
|
||||||
}
|
}
|
||||||
@ -40,7 +41,7 @@ export default {
|
|||||||
}
|
}
|
||||||
return resolve(root, args, context, info)
|
return resolve(root, args, context, info)
|
||||||
},
|
},
|
||||||
Post: obfuscateDisabled,
|
Post: obfuscate,
|
||||||
User: obfuscateDisabled,
|
User: obfuscate,
|
||||||
Comment: obfuscateDisabled,
|
Comment: obfuscate,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,49 +1,70 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
|
||||||
import Factory from '../seed/factories'
|
import Factory from '../seed/factories'
|
||||||
import { host, login } from '../jest/helpers'
|
import { gql } from '../jest/helpers'
|
||||||
import { neode } from '../bootstrap/neo4j'
|
import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
|
||||||
|
import createServer from '../server'
|
||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
const instance = neode()
|
const neode = getNeode()
|
||||||
|
const driver = getDriver()
|
||||||
|
|
||||||
let client
|
|
||||||
let query
|
let query
|
||||||
let action
|
let mutate
|
||||||
|
let graphqlQuery
|
||||||
const categoryIds = ['cat9']
|
const categoryIds = ['cat9']
|
||||||
|
let authenticatedUser
|
||||||
|
let user
|
||||||
|
let moderator
|
||||||
|
let troll
|
||||||
|
|
||||||
|
const action = () => {
|
||||||
|
return query({ query: graphqlQuery })
|
||||||
|
}
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// For performance reasons we do this only once
|
// For performance reasons we do this only once
|
||||||
await Promise.all([
|
const users = await Promise.all([
|
||||||
factory.create('User', { id: 'u1', role: 'user', email: 'user@example.org', password: '1234' }),
|
factory.create('User', { id: 'u1', role: 'user' }),
|
||||||
factory.create('User', {
|
factory.create('User', {
|
||||||
id: 'm1',
|
id: 'm1',
|
||||||
role: 'moderator',
|
role: 'moderator',
|
||||||
email: 'moderator@example.org',
|
|
||||||
password: '1234',
|
password: '1234',
|
||||||
}),
|
}),
|
||||||
factory.create('User', {
|
factory.create('User', {
|
||||||
id: 'u2',
|
id: 'u2',
|
||||||
role: 'user',
|
role: 'user',
|
||||||
name: 'Offensive Name',
|
name: 'Offensive Name',
|
||||||
|
slug: 'offensive-name',
|
||||||
avatar: '/some/offensive/avatar.jpg',
|
avatar: '/some/offensive/avatar.jpg',
|
||||||
about: 'This self description is very offensive',
|
about: 'This self description is very offensive',
|
||||||
email: 'troll@example.org',
|
|
||||||
password: '1234',
|
|
||||||
}),
|
|
||||||
instance.create('Category', {
|
|
||||||
id: 'cat9',
|
|
||||||
name: 'Democracy & Politics',
|
|
||||||
icon: 'university',
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await factory.authenticateAs({ email: 'user@example.org', password: '1234' })
|
user = users[0]
|
||||||
|
moderator = users[1]
|
||||||
|
troll = users[2]
|
||||||
|
|
||||||
|
await neode.create('Category', {
|
||||||
|
id: 'cat9',
|
||||||
|
name: 'Democracy & Politics',
|
||||||
|
icon: 'university',
|
||||||
|
})
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
factory.follow({ id: 'u2', type: 'User' }),
|
user.relateTo(troll, 'following'),
|
||||||
factory.create('Post', { id: 'p1', title: 'Deleted post', deleted: true, categoryIds }),
|
|
||||||
factory.create('Post', {
|
factory.create('Post', {
|
||||||
|
author: user,
|
||||||
|
id: 'p1',
|
||||||
|
title: 'Deleted post',
|
||||||
|
slug: 'deleted-post',
|
||||||
|
deleted: true,
|
||||||
|
categoryIds,
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author: user,
|
||||||
id: 'p3',
|
id: 'p3',
|
||||||
title: 'Publicly visible post',
|
title: 'Publicly visible post',
|
||||||
|
slug: 'publicly-visible-post',
|
||||||
deleted: false,
|
deleted: false,
|
||||||
categoryIds,
|
categoryIds,
|
||||||
}),
|
}),
|
||||||
@ -51,32 +72,56 @@ beforeAll(async () => {
|
|||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
factory.create('Comment', {
|
factory.create('Comment', {
|
||||||
|
author: user,
|
||||||
id: 'c2',
|
id: 'c2',
|
||||||
postId: 'p3',
|
postId: 'p3',
|
||||||
content: 'Enabled comment on public post',
|
content: 'Enabled comment on public post',
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' })])
|
await factory.create('Post', {
|
||||||
|
|
||||||
const asTroll = Factory()
|
|
||||||
await asTroll.authenticateAs({ email: 'troll@example.org', password: '1234' })
|
|
||||||
await asTroll.create('Post', {
|
|
||||||
id: 'p2',
|
id: 'p2',
|
||||||
|
author: troll,
|
||||||
title: 'Disabled post',
|
title: 'Disabled post',
|
||||||
content: 'This is an offensive post content',
|
content: 'This is an offensive post content',
|
||||||
|
contentExcerpt: 'This is an offensive post content',
|
||||||
image: '/some/offensive/image.jpg',
|
image: '/some/offensive/image.jpg',
|
||||||
deleted: false,
|
deleted: false,
|
||||||
categoryIds,
|
categoryIds,
|
||||||
})
|
})
|
||||||
await asTroll.create('Comment', { id: 'c1', postId: 'p3', content: 'Disabled comment' })
|
await factory.create('Comment', {
|
||||||
await Promise.all([asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' })])
|
id: 'c1',
|
||||||
|
author: troll,
|
||||||
|
postId: 'p3',
|
||||||
|
content: 'Disabled comment',
|
||||||
|
contentExcerpt: 'Disabled comment',
|
||||||
|
})
|
||||||
|
|
||||||
const asModerator = Factory()
|
const { server } = createServer({
|
||||||
await asModerator.authenticateAs({ email: 'moderator@example.org', password: '1234' })
|
context: () => {
|
||||||
await asModerator.mutate('mutation { disable( id: "p2") }')
|
return {
|
||||||
await asModerator.mutate('mutation { disable( id: "c1") }')
|
driver,
|
||||||
await asModerator.mutate('mutation { disable( id: "u2") }')
|
neode,
|
||||||
|
user: authenticatedUser,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const client = createTestClient(server)
|
||||||
|
query = client.query
|
||||||
|
mutate = client.mutate
|
||||||
|
|
||||||
|
authenticatedUser = await moderator.toJson()
|
||||||
|
const disableMutation = gql`
|
||||||
|
mutation($id: ID!) {
|
||||||
|
disable(id: $id)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
await Promise.all([
|
||||||
|
mutate({ mutation: disableMutation, variables: { id: 'c1' } }),
|
||||||
|
mutate({ mutation: disableMutation, variables: { id: 'u2' } }),
|
||||||
|
mutate({ mutation: disableMutation, variables: { id: 'p2' } }),
|
||||||
|
])
|
||||||
|
authenticatedUser = null
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -85,93 +130,124 @@ afterAll(async () => {
|
|||||||
|
|
||||||
describe('softDeleteMiddleware', () => {
|
describe('softDeleteMiddleware', () => {
|
||||||
describe('read disabled content', () => {
|
describe('read disabled content', () => {
|
||||||
let user
|
let subject
|
||||||
let post
|
|
||||||
let comment
|
|
||||||
const beforeComment = async () => {
|
const beforeComment = async () => {
|
||||||
query = '{ User(id: "u1") { following { comments { content contentExcerpt } } } }'
|
graphqlQuery = gql`
|
||||||
const response = await action()
|
{
|
||||||
comment = response.User[0].following[0].comments[0]
|
User(id: "u1") {
|
||||||
|
following {
|
||||||
|
comments {
|
||||||
|
content
|
||||||
|
contentExcerpt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const { data } = await action()
|
||||||
|
subject = data.User[0].following[0].comments[0]
|
||||||
}
|
}
|
||||||
const beforeUser = async () => {
|
const beforeUser = async () => {
|
||||||
query = '{ User(id: "u1") { following { name about avatar } } }'
|
graphqlQuery = gql`
|
||||||
const response = await action()
|
{
|
||||||
user = response.User[0].following[0]
|
User(id: "u1") {
|
||||||
|
following {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
about
|
||||||
|
avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const { data } = await action()
|
||||||
|
subject = data.User[0].following[0]
|
||||||
}
|
}
|
||||||
const beforePost = async () => {
|
const beforePost = async () => {
|
||||||
query =
|
graphqlQuery = gql`
|
||||||
'{ User(id: "u1") { following { contributions { title image content contentExcerpt } } } }'
|
{
|
||||||
const response = await action()
|
User(id: "u1") {
|
||||||
post = response.User[0].following[0].contributions[0]
|
following {
|
||||||
}
|
contributions {
|
||||||
|
title
|
||||||
action = () => {
|
slug
|
||||||
return client.request(query)
|
image
|
||||||
|
content
|
||||||
|
contentExcerpt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const { data } = await action()
|
||||||
|
subject = data.User[0].following[0].contributions[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('as moderator', () => {
|
describe('as moderator', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
authenticatedUser = await moderator.toJson()
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('User', () => {
|
describe('User', () => {
|
||||||
beforeEach(beforeUser)
|
beforeEach(beforeUser)
|
||||||
|
|
||||||
it('displays name', () => expect(user.name).toEqual('Offensive Name'))
|
it('displays name', () => expect(subject.name).toEqual('Offensive Name'))
|
||||||
|
it('displays slug', () => expect(subject.slug).toEqual('offensive-name'))
|
||||||
it('displays about', () =>
|
it('displays about', () =>
|
||||||
expect(user.about).toEqual('This self description is very offensive'))
|
expect(subject.about).toEqual('This self description is very offensive'))
|
||||||
it('displays avatar', () => expect(user.avatar).toEqual('/some/offensive/avatar.jpg'))
|
it('displays avatar', () => expect(subject.avatar).toEqual('/some/offensive/avatar.jpg'))
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Post', () => {
|
describe('Post', () => {
|
||||||
beforeEach(beforePost)
|
beforeEach(beforePost)
|
||||||
|
|
||||||
it('displays title', () => expect(post.title).toEqual('Disabled post'))
|
it('displays title', () => expect(subject.title).toEqual('Disabled post'))
|
||||||
|
it('displays slug', () => expect(subject.slug).toEqual('disabled-post'))
|
||||||
it('displays content', () =>
|
it('displays content', () =>
|
||||||
expect(post.content).toEqual('This is an offensive post content'))
|
expect(subject.content).toEqual('This is an offensive post content'))
|
||||||
it('displays contentExcerpt', () =>
|
it('displays contentExcerpt', () =>
|
||||||
expect(post.contentExcerpt).toEqual('This is an offensive post content'))
|
expect(subject.contentExcerpt).toEqual('This is an offensive post content'))
|
||||||
it('displays image', () => expect(post.image).toEqual('/some/offensive/image.jpg'))
|
it('displays image', () => expect(subject.image).toEqual('/some/offensive/image.jpg'))
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Comment', () => {
|
describe('Comment', () => {
|
||||||
beforeEach(beforeComment)
|
beforeEach(beforeComment)
|
||||||
|
|
||||||
it('displays content', () => expect(comment.content).toEqual('Disabled comment'))
|
it('displays content', () => expect(subject.content).toEqual('Disabled comment'))
|
||||||
it('displays contentExcerpt', () =>
|
it('displays contentExcerpt', () =>
|
||||||
expect(comment.contentExcerpt).toEqual('Disabled comment'))
|
expect(subject.contentExcerpt).toEqual('Disabled comment'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('as user', () => {
|
describe('as user', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
authenticatedUser = await user.toJson()
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('User', () => {
|
describe('User', () => {
|
||||||
beforeEach(beforeUser)
|
beforeEach(beforeUser)
|
||||||
|
|
||||||
it('displays name', () => expect(user.name).toEqual('UNAVAILABLE'))
|
it('obfuscates name', () => expect(subject.name).toEqual('UNAVAILABLE'))
|
||||||
it('obfuscates about', () => expect(user.about).toEqual('UNAVAILABLE'))
|
it('obfuscates slug', () => expect(subject.slug).toEqual('UNAVAILABLE'))
|
||||||
it('obfuscates avatar', () => expect(user.avatar).toEqual('UNAVAILABLE'))
|
it('obfuscates about', () => expect(subject.about).toEqual('UNAVAILABLE'))
|
||||||
|
it('obfuscates avatar', () => expect(subject.avatar).toEqual('UNAVAILABLE'))
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Post', () => {
|
describe('Post', () => {
|
||||||
beforeEach(beforePost)
|
beforeEach(beforePost)
|
||||||
|
|
||||||
it('obfuscates title', () => expect(post.title).toEqual('UNAVAILABLE'))
|
it('obfuscates title', () => expect(subject.title).toEqual('UNAVAILABLE'))
|
||||||
it('obfuscates content', () => expect(post.content).toEqual('UNAVAILABLE'))
|
it('obfuscates slug', () => expect(subject.slug).toEqual('UNAVAILABLE'))
|
||||||
it('obfuscates contentExcerpt', () => expect(post.contentExcerpt).toEqual('UNAVAILABLE'))
|
it('obfuscates content', () => expect(subject.content).toEqual('UNAVAILABLE'))
|
||||||
it('obfuscates image', () => expect(post.image).toEqual('UNAVAILABLE'))
|
it('obfuscates contentExcerpt', () => expect(subject.contentExcerpt).toEqual('UNAVAILABLE'))
|
||||||
|
it('obfuscates image', () => expect(subject.image).toEqual(null))
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Comment', () => {
|
describe('Comment', () => {
|
||||||
beforeEach(beforeComment)
|
beforeEach(beforeComment)
|
||||||
|
|
||||||
it('obfuscates content', () => expect(comment.content).toEqual('UNAVAILABLE'))
|
it('obfuscates content', () => expect(subject.content).toEqual('UNAVAILABLE'))
|
||||||
it('obfuscates contentExcerpt', () => expect(comment.contentExcerpt).toEqual('UNAVAILABLE'))
|
it('obfuscates contentExcerpt', () => expect(subject.contentExcerpt).toEqual('UNAVAILABLE'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -179,43 +255,57 @@ describe('softDeleteMiddleware', () => {
|
|||||||
describe('Query', () => {
|
describe('Query', () => {
|
||||||
describe('Post', () => {
|
describe('Post', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
query = '{ Post { title } }'
|
graphqlQuery = gql`
|
||||||
|
{
|
||||||
|
Post {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('as user', () => {
|
describe('as user', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
authenticatedUser = await user.toJson()
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('hides deleted or disabled posts', async () => {
|
it('hides deleted or disabled posts', async () => {
|
||||||
const expected = { Post: [{ title: 'Publicly visible post' }] }
|
const expected = { data: { Post: [{ title: 'Publicly visible post' }] } }
|
||||||
await expect(action()).resolves.toEqual(expected)
|
await expect(action()).resolves.toMatchObject(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('as moderator', () => {
|
describe('as moderator', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
authenticatedUser = await moderator.toJson()
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows disabled but hides deleted posts', async () => {
|
it('shows disabled but hides deleted posts', async () => {
|
||||||
const expected = [{ title: 'Disabled post' }, { title: 'Publicly visible post' }]
|
const expected = [{ title: 'Disabled post' }, { title: 'Publicly visible post' }]
|
||||||
const { Post } = await action()
|
const {
|
||||||
|
data: { Post },
|
||||||
|
} = await action()
|
||||||
await expect(Post).toEqual(expect.arrayContaining(expected))
|
await expect(Post).toEqual(expect.arrayContaining(expected))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('.comments', () => {
|
describe('.comments', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
query = '{ Post(id: "p3") { title comments { content } } }'
|
graphqlQuery = gql`
|
||||||
|
{
|
||||||
|
Post(id: "p3") {
|
||||||
|
title
|
||||||
|
comments {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('as user', () => {
|
describe('as user', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
authenticatedUser = await user.toJson()
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('conceals disabled comments', async () => {
|
it('conceals disabled comments', async () => {
|
||||||
@ -224,7 +314,9 @@ describe('softDeleteMiddleware', () => {
|
|||||||
{ content: 'UNAVAILABLE' },
|
{ content: 'UNAVAILABLE' },
|
||||||
]
|
]
|
||||||
const {
|
const {
|
||||||
Post: [{ comments }],
|
data: {
|
||||||
|
Post: [{ comments }],
|
||||||
|
},
|
||||||
} = await action()
|
} = await action()
|
||||||
await expect(comments).toEqual(expect.arrayContaining(expected))
|
await expect(comments).toEqual(expect.arrayContaining(expected))
|
||||||
})
|
})
|
||||||
@ -232,8 +324,7 @@ describe('softDeleteMiddleware', () => {
|
|||||||
|
|
||||||
describe('as moderator', () => {
|
describe('as moderator', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
authenticatedUser = await moderator.toJson()
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows disabled comments', async () => {
|
it('shows disabled comments', async () => {
|
||||||
@ -242,7 +333,9 @@ describe('softDeleteMiddleware', () => {
|
|||||||
{ content: 'Disabled comment' },
|
{ content: 'Disabled comment' },
|
||||||
]
|
]
|
||||||
const {
|
const {
|
||||||
Post: [{ comments }],
|
data: {
|
||||||
|
Post: [{ comments }],
|
||||||
|
},
|
||||||
} = await action()
|
} = await action()
|
||||||
await expect(comments).toEqual(expect.arrayContaining(expected))
|
await expect(comments).toEqual(expect.arrayContaining(expected))
|
||||||
})
|
})
|
||||||
@ -251,58 +344,70 @@ describe('softDeleteMiddleware', () => {
|
|||||||
|
|
||||||
describe('filter (deleted: true)', () => {
|
describe('filter (deleted: true)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
query = '{ Post(deleted: true) { title } }'
|
graphqlQuery = gql`
|
||||||
|
{
|
||||||
|
Post(deleted: true) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('as user', () => {
|
describe('as user', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
authenticatedUser = await user.toJson()
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws authorisation error', async () => {
|
it('throws authorisation error', async () => {
|
||||||
await expect(action()).rejects.toThrow('Not Authorised!')
|
const { data, errors } = await action()
|
||||||
|
expect(data).toEqual({ Post: null })
|
||||||
|
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('as moderator', () => {
|
describe('as moderator', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
authenticatedUser = await moderator.toJson()
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows deleted posts', async () => {
|
it('does not show deleted posts', async () => {
|
||||||
const expected = { Post: [{ title: 'Deleted post' }] }
|
const expected = { data: { Post: [{ title: 'UNAVAILABLE' }] } }
|
||||||
await expect(action()).resolves.toEqual(expected)
|
await expect(action()).resolves.toMatchObject(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('filter (disabled: true)', () => {
|
describe('filter (disabled: true)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
query = '{ Post(disabled: true) { title } }'
|
graphqlQuery = gql`
|
||||||
|
{
|
||||||
|
Post(disabled: true) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('as user', () => {
|
describe('as user', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login({ email: 'user@example.org', password: '1234' })
|
authenticatedUser = await user.toJson()
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws authorisation error', async () => {
|
it('throws authorisation error', async () => {
|
||||||
await expect(action()).rejects.toThrow('Not Authorised!')
|
const { data, errors } = await action()
|
||||||
|
expect(data).toEqual({ Post: null })
|
||||||
|
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('as moderator', () => {
|
describe('as moderator', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
authenticatedUser = await moderator.toJson()
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows disabled posts', async () => {
|
it('shows disabled posts', async () => {
|
||||||
const expected = { Post: [{ title: 'Disabled post' }] }
|
const expected = { data: { Post: [{ title: 'Disabled post' }] } }
|
||||||
await expect(action()).resolves.toEqual(expected)
|
await expect(action()).resolves.toMatchObject(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -52,29 +52,9 @@ const validatePost = async (resolve, root, args, context, info) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const validateUpdatePost = async (resolve, root, args, context, info) => {
|
const validateUpdatePost = async (resolve, root, args, context, info) => {
|
||||||
const { id, categoryIds } = args
|
const { categoryIds } = args
|
||||||
const session = context.driver.session()
|
if (typeof categoryIds === 'undefined') return resolve(root, args, context, info)
|
||||||
const categoryQueryRes = await session.run(
|
return validatePost(resolve, root, args, context, info)
|
||||||
`
|
|
||||||
MATCH (post:Post {id: $id})-[:CATEGORIZED]->(category:Category)
|
|
||||||
RETURN category`,
|
|
||||||
{ id },
|
|
||||||
)
|
|
||||||
session.close()
|
|
||||||
const [category] = categoryQueryRes.records.map(record => {
|
|
||||||
return record.get('category')
|
|
||||||
})
|
|
||||||
|
|
||||||
if (category) {
|
|
||||||
if (categoryIds && categoryIds.length > 3) {
|
|
||||||
throw new UserInputError(NO_CATEGORIES_ERR_MESSAGE)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!Array.isArray(categoryIds) || !categoryIds.length || categoryIds.length > 3) {
|
|
||||||
throw new UserInputError(NO_CATEGORIES_ERR_MESSAGE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resolve(root, args, context, info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
48
backend/src/models/Comment.js
Normal file
48
backend/src/models/Comment.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import uuid from 'uuid/v4'
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
id: { type: 'string', primary: true, default: uuid },
|
||||||
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
updatedAt: {
|
||||||
|
type: 'string',
|
||||||
|
isoDate: true,
|
||||||
|
required: true,
|
||||||
|
default: () => new Date().toISOString(),
|
||||||
|
},
|
||||||
|
content: { type: 'string', disallow: [null], min: 3 },
|
||||||
|
contentExcerpt: { type: 'string', allow: [null] },
|
||||||
|
deleted: { type: 'boolean', default: false },
|
||||||
|
disabled: { type: 'boolean', default: false },
|
||||||
|
post: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'COMMENTS',
|
||||||
|
target: 'Post',
|
||||||
|
direction: 'out',
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'WROTE',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'in',
|
||||||
|
},
|
||||||
|
disabledBy: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'DISABLED',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'in',
|
||||||
|
},
|
||||||
|
notified: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'NOTIFIED',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'out',
|
||||||
|
properties: {
|
||||||
|
read: { type: 'boolean', default: false },
|
||||||
|
reason: {
|
||||||
|
type: 'string',
|
||||||
|
valid: ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post'],
|
||||||
|
},
|
||||||
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
21
backend/src/models/Location.js
Normal file
21
backend/src/models/Location.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
id: { type: 'string', primary: true },
|
||||||
|
lat: { type: 'number' },
|
||||||
|
lng: { type: 'number' },
|
||||||
|
type: { type: 'string' },
|
||||||
|
name: { type: 'string' },
|
||||||
|
nameES: { type: 'string' },
|
||||||
|
nameFR: { type: 'string' },
|
||||||
|
nameIT: { type: 'string' },
|
||||||
|
nameEN: { type: 'string' },
|
||||||
|
namePT: { type: 'string' },
|
||||||
|
nameDE: { type: 'string' },
|
||||||
|
nameNL: { type: 'string' },
|
||||||
|
namePL: { type: 'string' },
|
||||||
|
isIn: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'IS_IN',
|
||||||
|
target: 'Location',
|
||||||
|
direction: 'out',
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import uuid from 'uuid/v4'
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
id: {
|
|
||||||
type: 'uuid',
|
|
||||||
primary: true,
|
|
||||||
default: uuid,
|
|
||||||
},
|
|
||||||
read: {
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
reason: {
|
|
||||||
type: 'string',
|
|
||||||
valid: ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_post'],
|
|
||||||
invalid: [null],
|
|
||||||
default: 'mentioned_in_post',
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: 'string',
|
|
||||||
isoDate: true,
|
|
||||||
default: () => new Date().toISOString(),
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
type: 'relationship',
|
|
||||||
relationship: 'NOTIFIED',
|
|
||||||
target: 'User',
|
|
||||||
direction: 'out',
|
|
||||||
},
|
|
||||||
post: {
|
|
||||||
type: 'relationship',
|
|
||||||
relationship: 'NOTIFIED',
|
|
||||||
target: 'Post',
|
|
||||||
direction: 'in',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@ -23,6 +23,20 @@ module.exports = {
|
|||||||
target: 'User',
|
target: 'User',
|
||||||
direction: 'in',
|
direction: 'in',
|
||||||
},
|
},
|
||||||
|
notified: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'NOTIFIED',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'out',
|
||||||
|
properties: {
|
||||||
|
read: { type: 'boolean', default: false },
|
||||||
|
reason: {
|
||||||
|
type: 'string',
|
||||||
|
valid: ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post'],
|
||||||
|
},
|
||||||
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
},
|
||||||
|
},
|
||||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|||||||
@ -8,7 +8,7 @@ module.exports = {
|
|||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationship: 'OWNED_BY',
|
relationship: 'OWNED_BY',
|
||||||
target: 'User',
|
target: 'User',
|
||||||
direction: 'in',
|
direction: 'out',
|
||||||
eager: true,
|
eager: true,
|
||||||
cascade: 'detach',
|
cascade: 'detach',
|
||||||
},
|
},
|
||||||
|
|||||||
17
backend/src/models/Tag.js
Normal file
17
backend/src/models/Tag.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module.exports = {
|
||||||
|
id: { type: 'string', primary: true },
|
||||||
|
deleted: { type: 'boolean', default: false },
|
||||||
|
disabled: { type: 'boolean', default: false },
|
||||||
|
updatedAt: {
|
||||||
|
type: 'string',
|
||||||
|
isoDate: true,
|
||||||
|
required: true,
|
||||||
|
default: () => new Date().toISOString(),
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'TAGGED',
|
||||||
|
target: 'Post',
|
||||||
|
direction: 'in',
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -83,4 +83,16 @@ module.exports = {
|
|||||||
target: 'Notification',
|
target: 'Notification',
|
||||||
direction: 'in',
|
direction: 'in',
|
||||||
},
|
},
|
||||||
|
shouted: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'SHOUTED',
|
||||||
|
target: 'Post',
|
||||||
|
direction: 'out',
|
||||||
|
},
|
||||||
|
isIn: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'IS_IN',
|
||||||
|
target: 'Location',
|
||||||
|
direction: 'out',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,8 @@ export default {
|
|||||||
EmailAddress: require('./EmailAddress.js'),
|
EmailAddress: require('./EmailAddress.js'),
|
||||||
SocialMedia: require('./SocialMedia.js'),
|
SocialMedia: require('./SocialMedia.js'),
|
||||||
Post: require('./Post.js'),
|
Post: require('./Post.js'),
|
||||||
Notification: require('./Notification.js'),
|
Comment: require('./Comment.js'),
|
||||||
Category: require('./Category.js'),
|
Category: require('./Category.js'),
|
||||||
|
Tag: require('./Tag.js'),
|
||||||
|
Location: require('./Location.js'),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export default applyScalars(
|
|||||||
'Statistics',
|
'Statistics',
|
||||||
'LoggedInUser',
|
'LoggedInUser',
|
||||||
'SocialMedia',
|
'SocialMedia',
|
||||||
|
'NOTIFIED',
|
||||||
],
|
],
|
||||||
// add 'User' here as soon as possible
|
// add 'User' here as soon as possible
|
||||||
},
|
},
|
||||||
@ -32,6 +33,7 @@ export default applyScalars(
|
|||||||
'Statistics',
|
'Statistics',
|
||||||
'LoggedInUser',
|
'LoggedInUser',
|
||||||
'SocialMedia',
|
'SocialMedia',
|
||||||
|
'NOTIFIED',
|
||||||
],
|
],
|
||||||
// add 'User' here as soon as possible
|
// add 'User' here as soon as possible
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
|
import Resolver from './helpers/Resolver'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
@ -46,10 +47,29 @@ export default {
|
|||||||
session.close()
|
session.close()
|
||||||
return commentReturnedWithAuthor
|
return commentReturnedWithAuthor
|
||||||
},
|
},
|
||||||
DeleteComment: async (object, params, context, resolveInfo) => {
|
DeleteComment: async (object, args, context, resolveInfo) => {
|
||||||
const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
|
const session = context.driver.session()
|
||||||
|
const transactionRes = await session.run(
|
||||||
|
`
|
||||||
|
MATCH (comment:Comment {id: $commentId})
|
||||||
|
SET comment.deleted = TRUE
|
||||||
|
SET comment.content = 'UNAVAILABLE'
|
||||||
|
SET comment.contentExcerpt = 'UNAVAILABLE'
|
||||||
|
RETURN comment
|
||||||
|
`,
|
||||||
|
{ commentId: args.id },
|
||||||
|
)
|
||||||
|
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||||
return comment
|
return comment
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Comment: {
|
||||||
|
...Resolver('Comment', {
|
||||||
|
hasOne: {
|
||||||
|
author: '<-[:WROTE]-(related:User)',
|
||||||
|
post: '-[:COMMENTS]->(related:Post)',
|
||||||
|
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +1,34 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import { host, login, gql } from '../../jest/helpers'
|
import { gql } from '../../jest/helpers'
|
||||||
import { neode } from '../../bootstrap/neo4j'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import createServer from '../../server'
|
||||||
|
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||||
|
|
||||||
let client
|
const driver = getDriver()
|
||||||
let createCommentVariables
|
const neode = getNeode()
|
||||||
let createCommentVariablesSansPostId
|
|
||||||
let createCommentVariablesWithNonExistentPost
|
|
||||||
let userParams
|
|
||||||
let headers
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
const instance = neode()
|
|
||||||
const categoryIds = ['cat9']
|
|
||||||
|
|
||||||
const createPostMutation = gql`
|
let variables
|
||||||
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]!) {
|
let mutate
|
||||||
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
let authenticatedUser
|
||||||
id
|
let commentAuthor
|
||||||
}
|
|
||||||
}
|
beforeAll(() => {
|
||||||
`
|
const { server } = createServer({
|
||||||
const createCommentMutation = gql`
|
context: () => {
|
||||||
mutation($id: ID, $postId: ID!, $content: String!) {
|
return {
|
||||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
driver,
|
||||||
id
|
user: authenticatedUser,
|
||||||
content
|
}
|
||||||
}
|
},
|
||||||
}
|
})
|
||||||
`
|
const client = createTestClient(server)
|
||||||
const createPostVariables = {
|
mutate = client.mutate
|
||||||
id: 'p1',
|
})
|
||||||
title: 'post to comment on',
|
|
||||||
content: 'please comment on me',
|
|
||||||
categoryIds,
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
userParams = {
|
variables = {}
|
||||||
name: 'TestUser',
|
await neode.create('Category', {
|
||||||
email: 'test@example.org',
|
|
||||||
password: '1234',
|
|
||||||
}
|
|
||||||
await factory.create('User', userParams)
|
|
||||||
await instance.create('Category', {
|
|
||||||
id: 'cat9',
|
id: 'cat9',
|
||||||
name: 'Democracy & Politics',
|
name: 'Democracy & Politics',
|
||||||
icon: 'university',
|
icon: 'university',
|
||||||
@ -53,335 +39,243 @@ afterEach(async () => {
|
|||||||
await factory.cleanDatabase()
|
await factory.cleanDatabase()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const createCommentMutation = gql`
|
||||||
|
mutation($id: ID, $postId: ID!, $content: String!) {
|
||||||
|
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||||
|
id
|
||||||
|
content
|
||||||
|
author {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const setupPostAndComment = async () => {
|
||||||
|
commentAuthor = await factory.create('User')
|
||||||
|
await factory.create('Post', {
|
||||||
|
id: 'p1',
|
||||||
|
content: 'Post to be commented',
|
||||||
|
categoryIds: ['cat9'],
|
||||||
|
})
|
||||||
|
await factory.create('Comment', {
|
||||||
|
id: 'c456',
|
||||||
|
postId: 'p1',
|
||||||
|
author: commentAuthor,
|
||||||
|
content: 'Comment to be deleted',
|
||||||
|
})
|
||||||
|
variables = {
|
||||||
|
...variables,
|
||||||
|
id: 'c456',
|
||||||
|
content: 'The comment is updated',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('CreateComment', () => {
|
describe('CreateComment', () => {
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
createCommentVariables = {
|
variables = {
|
||||||
|
...variables,
|
||||||
postId: 'p1',
|
postId: 'p1',
|
||||||
content: "I'm not authorised to comment",
|
content: "I'm not authorised to comment",
|
||||||
}
|
}
|
||||||
client = new GraphQLClient(host)
|
const { errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||||
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
|
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
'Not Authorised',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login(userParams)
|
const user = await neode.create('User', { name: 'Author' })
|
||||||
client = new GraphQLClient(host, {
|
authenticatedUser = await user.toJson()
|
||||||
headers,
|
|
||||||
})
|
|
||||||
createCommentVariables = {
|
|
||||||
postId: 'p1',
|
|
||||||
content: "I'm authorised to comment",
|
|
||||||
}
|
|
||||||
await client.request(createPostMutation, createPostVariables)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates a comment', async () => {
|
describe('given a post', () => {
|
||||||
const expected = {
|
beforeEach(async () => {
|
||||||
CreateComment: {
|
await factory.create('Post', { categoryIds: ['cat9'], id: 'p1' })
|
||||||
|
variables = {
|
||||||
|
...variables,
|
||||||
|
postId: 'p1',
|
||||||
content: "I'm authorised to comment",
|
content: "I'm authorised to comment",
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
client.request(createCommentMutation, createCommentVariables),
|
|
||||||
).resolves.toMatchObject(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('assigns the authenticated user as author', async () => {
|
|
||||||
await client.request(createCommentMutation, createCommentVariables)
|
|
||||||
|
|
||||||
const { User } = await client.request(gql`
|
|
||||||
{
|
|
||||||
User(name: "TestUser") {
|
|
||||||
comments {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`)
|
})
|
||||||
|
|
||||||
expect(User).toEqual([
|
it('creates a comment', async () => {
|
||||||
{
|
await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
|
||||||
comments: [
|
{
|
||||||
{
|
data: { CreateComment: { content: "I'm authorised to comment" } },
|
||||||
content: "I'm authorised to comment",
|
},
|
||||||
},
|
)
|
||||||
],
|
})
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throw an error if an empty string is sent from the editor as content', async () => {
|
it('assigns the authenticated user as author', async () => {
|
||||||
createCommentVariables = {
|
await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
|
||||||
postId: 'p1',
|
{
|
||||||
content: '<p></p>',
|
data: { CreateComment: { author: { name: 'Author' } } },
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
|
describe('comment content is empty', () => {
|
||||||
'Comment must be at least 1 character long!',
|
beforeEach(() => {
|
||||||
)
|
variables = { ...variables, content: '<p></p>' }
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error if a comment sent from the editor does not contain a single character', async () => {
|
it('throw UserInput error', async () => {
|
||||||
createCommentVariables = {
|
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||||
postId: 'p1',
|
expect(data).toEqual({ CreateComment: null })
|
||||||
content: '<p> </p>',
|
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
||||||
}
|
})
|
||||||
|
})
|
||||||
|
|
||||||
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
|
describe('comment content contains only whitespaces', () => {
|
||||||
'Comment must be at least 1 character long!',
|
beforeEach(() => {
|
||||||
)
|
variables = { ...variables, content: ' <p> </p> ' }
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error if postId is sent as an empty string', async () => {
|
it('throw UserInput error', async () => {
|
||||||
createCommentVariables = {
|
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||||
postId: 'p1',
|
expect(data).toEqual({ CreateComment: null })
|
||||||
content: '',
|
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
||||||
}
|
})
|
||||||
|
})
|
||||||
|
|
||||||
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
|
describe('invalid post id', () => {
|
||||||
'Comment must be at least 1 character long!',
|
beforeEach(() => {
|
||||||
)
|
variables = { ...variables, postId: 'does-not-exist' }
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error if content is sent as an string of empty characters', async () => {
|
it('throw UserInput error', async () => {
|
||||||
createCommentVariables = {
|
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||||
postId: 'p1',
|
expect(data).toEqual({ CreateComment: null })
|
||||||
content: ' ',
|
expect(errors[0]).toHaveProperty('message', 'Comment cannot be created without a post!')
|
||||||
}
|
})
|
||||||
|
})
|
||||||
await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow(
|
|
||||||
'Comment must be at least 1 character long!',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws an error if postId is sent as an empty string', async () => {
|
|
||||||
createCommentVariablesSansPostId = {
|
|
||||||
postId: '',
|
|
||||||
content: 'this comment should not be created',
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
client.request(createCommentMutation, createCommentVariablesSansPostId),
|
|
||||||
).rejects.toThrow('Comment cannot be created without a post!')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws an error if postId is sent as an string of empty characters', async () => {
|
|
||||||
createCommentVariablesSansPostId = {
|
|
||||||
postId: ' ',
|
|
||||||
content: 'this comment should not be created',
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
client.request(createCommentMutation, createCommentVariablesSansPostId),
|
|
||||||
).rejects.toThrow('Comment cannot be created without a post!')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws an error if the post does not exist in the database', async () => {
|
|
||||||
createCommentVariablesWithNonExistentPost = {
|
|
||||||
postId: 'p2',
|
|
||||||
content: "comment should not be created cause the post doesn't exist",
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
client.request(createCommentMutation, createCommentVariablesWithNonExistentPost),
|
|
||||||
).rejects.toThrow('Comment cannot be created without a post!')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('ManageComments', () => {
|
describe('UpdateComment', () => {
|
||||||
let authorParams
|
const updateCommentMutation = gql`
|
||||||
beforeEach(async () => {
|
mutation($content: String!, $id: ID!) {
|
||||||
authorParams = {
|
UpdateComment(content: $content, id: $id) {
|
||||||
email: 'author@example.org',
|
id
|
||||||
password: '1234',
|
content
|
||||||
}
|
|
||||||
const asAuthor = Factory()
|
|
||||||
await asAuthor.create('User', authorParams)
|
|
||||||
await asAuthor.authenticateAs(authorParams)
|
|
||||||
await asAuthor.create('Post', {
|
|
||||||
id: 'p1',
|
|
||||||
content: 'Post to be commented',
|
|
||||||
categoryIds,
|
|
||||||
})
|
|
||||||
await asAuthor.create('Comment', {
|
|
||||||
id: 'c456',
|
|
||||||
postId: 'p1',
|
|
||||||
content: 'Comment to be deleted',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('UpdateComment', () => {
|
|
||||||
const updateCommentMutation = gql`
|
|
||||||
mutation($content: String!, $id: ID!) {
|
|
||||||
UpdateComment(content: $content, id: $id) {
|
|
||||||
id
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`
|
|
||||||
|
|
||||||
let updateCommentVariables = {
|
|
||||||
id: 'c456',
|
|
||||||
content: 'The comment is updated',
|
|
||||||
}
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
describe('given a post and a comment', () => {
|
||||||
|
beforeEach(setupPostAndComment)
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
client = new GraphQLClient(host)
|
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||||
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
'Not Authorised',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated but not the author', () => {
|
describe('authenticated but not the author', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login({
|
const randomGuy = await factory.create('User')
|
||||||
email: 'test@example.org',
|
authenticatedUser = await randomGuy.toJson()
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
client = new GraphQLClient(host, {
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||||
'Not Authorised',
|
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated as author', () => {
|
describe('authenticated as author', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login(authorParams)
|
authenticatedUser = await commentAuthor.toJson()
|
||||||
client = new GraphQLClient(host, {
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates the comment', async () => {
|
it('updates the comment', async () => {
|
||||||
const expected = {
|
const expected = {
|
||||||
UpdateComment: {
|
data: { UpdateComment: { id: 'c456', content: 'The comment is updated' } },
|
||||||
id: 'c456',
|
|
||||||
content: 'The comment is updated',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
await expect(
|
await expect(mutate({ mutation: updateCommentMutation, variables })).resolves.toMatchObject(
|
||||||
client.request(updateCommentMutation, updateCommentVariables),
|
expected,
|
||||||
).resolves.toEqual(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throw an error if an empty string is sent from the editor as content', async () => {
|
|
||||||
updateCommentVariables = {
|
|
||||||
id: 'c456',
|
|
||||||
content: '<p></p>',
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
|
||||||
'Comment must be at least 1 character long!',
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error if a comment sent from the editor does not contain a single letter character', async () => {
|
describe('if `content` empty', () => {
|
||||||
updateCommentVariables = {
|
beforeEach(() => {
|
||||||
id: 'c456',
|
variables = { ...variables, content: ' <p> </p>' }
|
||||||
content: '<p> </p>',
|
})
|
||||||
}
|
|
||||||
|
|
||||||
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
it('throws InputError', async () => {
|
||||||
'Comment must be at least 1 character long!',
|
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||||
)
|
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error if commentId is sent as an empty string', async () => {
|
describe('if comment does not exist for given id', () => {
|
||||||
updateCommentVariables = {
|
beforeEach(() => {
|
||||||
id: '',
|
variables = { ...variables, id: 'does-not-exist' }
|
||||||
content: '<p>Hello</p>',
|
})
|
||||||
}
|
|
||||||
|
|
||||||
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
it('returns null', async () => {
|
||||||
'Not Authorised!',
|
const { data, errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||||
)
|
expect(data).toMatchObject({ UpdateComment: null })
|
||||||
})
|
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
it('throws an error if the comment does not exist in the database', async () => {
|
|
||||||
updateCommentVariables = {
|
|
||||||
id: 'c1000',
|
|
||||||
content: '<p>Hello</p>',
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
|
||||||
'Not Authorised!',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('DeleteComment', () => {
|
describe('DeleteComment', () => {
|
||||||
const deleteCommentMutation = gql`
|
const deleteCommentMutation = gql`
|
||||||
mutation($id: ID!) {
|
mutation($id: ID!) {
|
||||||
DeleteComment(id: $id) {
|
DeleteComment(id: $id) {
|
||||||
id
|
id
|
||||||
}
|
content
|
||||||
|
contentExcerpt
|
||||||
|
deleted
|
||||||
}
|
}
|
||||||
`
|
|
||||||
|
|
||||||
const deleteCommentVariables = {
|
|
||||||
id: 'c456',
|
|
||||||
}
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
describe('given a post and a comment', () => {
|
||||||
|
beforeEach(setupPostAndComment)
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
client = new GraphQLClient(host)
|
const result = await mutate({ mutation: deleteCommentMutation, variables })
|
||||||
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
'Not Authorised',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated but not the author', () => {
|
describe('authenticated but not the author', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login({
|
const randomGuy = await factory.create('User')
|
||||||
email: 'test@example.org',
|
authenticatedUser = await randomGuy.toJson()
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
client = new GraphQLClient(host, {
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
|
const { errors } = await mutate({ mutation: deleteCommentMutation, variables })
|
||||||
'Not Authorised',
|
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated as author', () => {
|
describe('authenticated as author', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login(authorParams)
|
authenticatedUser = await commentAuthor.toJson()
|
||||||
client = new GraphQLClient(host, {
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deletes the comment', async () => {
|
it('marks the comment as deleted and blacks out content', async () => {
|
||||||
|
const { data } = await mutate({ mutation: deleteCommentMutation, variables })
|
||||||
const expected = {
|
const expected = {
|
||||||
DeleteComment: {
|
DeleteComment: {
|
||||||
id: 'c456',
|
id: 'c456',
|
||||||
|
deleted: true,
|
||||||
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await expect(
|
expect(data).toMatchObject(expected)
|
||||||
client.request(deleteCommentMutation, deleteCommentVariables),
|
|
||||||
).resolves.toEqual(expected)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -61,7 +61,6 @@ export default function Resolver(type, options = {}) {
|
|||||||
const id = parent[idAttribute]
|
const id = parent[idAttribute]
|
||||||
const statement = `
|
const statement = `
|
||||||
MATCH(u:${type} {${idAttribute}: {id}})${connection}
|
MATCH(u:${type} {${idAttribute}: {id}})${connection}
|
||||||
WHERE NOT related.deleted = true AND NOT related.disabled = true
|
|
||||||
RETURN COUNT(DISTINCT(related)) as count
|
RETURN COUNT(DISTINCT(related)) as count
|
||||||
`
|
`
|
||||||
const result = await instance.cypher(statement, { id })
|
const result = await instance.cypher(statement, { id })
|
||||||
|
|||||||
@ -1,14 +1,80 @@
|
|||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
const resourceTypes = ['Post', 'Comment']
|
||||||
|
|
||||||
|
const transformReturnType = record => {
|
||||||
|
return {
|
||||||
|
...record.get('notification').properties,
|
||||||
|
from: {
|
||||||
|
__typename: record.get('resource').labels.find(l => resourceTypes.includes(l)),
|
||||||
|
...record.get('resource').properties,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
...record.get('user').properties,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
Notification: (object, params, context, resolveInfo) => {
|
notifications: async (parent, args, context, resolveInfo) => {
|
||||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
const { user: currentUser } = context
|
||||||
|
const session = context.driver.session()
|
||||||
|
let notifications
|
||||||
|
let whereClause
|
||||||
|
let orderByClause
|
||||||
|
switch (args.read) {
|
||||||
|
case true:
|
||||||
|
whereClause = 'WHERE notification.read = TRUE'
|
||||||
|
break
|
||||||
|
case false:
|
||||||
|
whereClause = 'WHERE notification.read = FALSE'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
whereClause = ''
|
||||||
|
}
|
||||||
|
switch (args.orderBy) {
|
||||||
|
case 'createdAt_asc':
|
||||||
|
orderByClause = 'ORDER BY notification.createdAt ASC'
|
||||||
|
break
|
||||||
|
case 'createdAt_desc':
|
||||||
|
orderByClause = 'ORDER BY notification.createdAt DESC'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
orderByClause = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cypher = `
|
||||||
|
MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id})
|
||||||
|
${whereClause}
|
||||||
|
RETURN resource, notification, user
|
||||||
|
${orderByClause}
|
||||||
|
`
|
||||||
|
const result = await session.run(cypher, { id: currentUser.id })
|
||||||
|
notifications = await result.records.map(transformReturnType)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
return notifications
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
UpdateNotification: (object, params, context, resolveInfo) => {
|
markAsRead: async (parent, args, context, resolveInfo) => {
|
||||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
const { user: currentUser } = context
|
||||||
|
const session = context.driver.session()
|
||||||
|
let notification
|
||||||
|
try {
|
||||||
|
const cypher = `
|
||||||
|
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
||||||
|
SET notification.read = TRUE
|
||||||
|
RETURN resource, notification, user
|
||||||
|
`
|
||||||
|
const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id })
|
||||||
|
const notifications = await result.records.map(transformReturnType)
|
||||||
|
notification = notifications[0]
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
return notification
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,395 +1,363 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import { host, login, gql } from '../../jest/helpers'
|
import { gql } from '../../jest/helpers'
|
||||||
import { neode } from '../../bootstrap/neo4j'
|
import { getDriver } from '../../bootstrap/neo4j'
|
||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import createServer from '../.././server'
|
||||||
|
|
||||||
let client
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
const instance = neode()
|
const driver = getDriver()
|
||||||
const userParams = {
|
let authenticatedUser
|
||||||
id: 'you',
|
let user
|
||||||
email: 'test@example.org',
|
let author
|
||||||
password: '1234',
|
let variables
|
||||||
}
|
let query
|
||||||
const categoryIds = ['cat9']
|
let mutate
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
driver,
|
||||||
|
user: authenticatedUser,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
query = createTestClient(server).query
|
||||||
|
mutate = createTestClient(server).mutate
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await factory.create('User', userParams)
|
authenticatedUser = null
|
||||||
await instance.create('Category', {
|
variables = { orderBy: 'createdAt_asc' }
|
||||||
id: 'cat9',
|
|
||||||
name: 'Democracy & Politics',
|
|
||||||
icon: 'university',
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await factory.cleanDatabase()
|
await factory.cleanDatabase()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Notification', () => {
|
describe('given some notifications', () => {
|
||||||
const notificationQuery = gql`
|
beforeEach(async () => {
|
||||||
query {
|
const categoryIds = ['cat1']
|
||||||
Notification {
|
author = await factory.create('User', { id: 'author' })
|
||||||
id
|
user = await factory.create('User', { id: 'you' })
|
||||||
}
|
const [neighbor] = await Promise.all([
|
||||||
}
|
factory.create('User', { id: 'neighbor' }),
|
||||||
`
|
factory.create('Category', { id: 'cat1' }),
|
||||||
|
])
|
||||||
describe('unauthenticated', () => {
|
const [post1, post2, post3] = await Promise.all([
|
||||||
it('throws authorization error', async () => {
|
factory.create('Post', { author, id: 'p1', categoryIds, content: 'Not for you' }),
|
||||||
client = new GraphQLClient(host)
|
factory.create('Post', {
|
||||||
await expect(client.request(notificationQuery)).rejects.toThrow('Not Authorised')
|
author,
|
||||||
})
|
id: 'p2',
|
||||||
|
categoryIds,
|
||||||
|
content: 'Already seen post mention',
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author,
|
||||||
|
id: 'p3',
|
||||||
|
categoryIds,
|
||||||
|
content: 'You have been mentioned in a post',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
const [comment1, comment2, comment3] = await Promise.all([
|
||||||
|
factory.create('Comment', {
|
||||||
|
author,
|
||||||
|
postId: 'p3',
|
||||||
|
id: 'c1',
|
||||||
|
content: 'You have seen this comment mentioning already',
|
||||||
|
}),
|
||||||
|
factory.create('Comment', {
|
||||||
|
author,
|
||||||
|
postId: 'p3',
|
||||||
|
id: 'c2',
|
||||||
|
content: 'You have been mentioned in a comment',
|
||||||
|
}),
|
||||||
|
factory.create('Comment', {
|
||||||
|
author,
|
||||||
|
postId: 'p3',
|
||||||
|
id: 'c3',
|
||||||
|
content: 'Somebody else was mentioned in a comment',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
await Promise.all([
|
||||||
|
post1.relateTo(neighbor, 'notified', {
|
||||||
|
createdAt: '2019-08-29T17:33:48.651Z',
|
||||||
|
read: false,
|
||||||
|
reason: 'mentioned_in_post',
|
||||||
|
}),
|
||||||
|
post2.relateTo(user, 'notified', {
|
||||||
|
createdAt: '2019-08-30T17:33:48.651Z',
|
||||||
|
read: true,
|
||||||
|
reason: 'mentioned_in_post',
|
||||||
|
}),
|
||||||
|
post3.relateTo(user, 'notified', {
|
||||||
|
createdAt: '2019-08-31T17:33:48.651Z',
|
||||||
|
read: false,
|
||||||
|
reason: 'mentioned_in_post',
|
||||||
|
}),
|
||||||
|
comment1.relateTo(user, 'notified', {
|
||||||
|
createdAt: '2019-08-30T15:33:48.651Z',
|
||||||
|
read: true,
|
||||||
|
reason: 'mentioned_in_comment',
|
||||||
|
}),
|
||||||
|
comment2.relateTo(user, 'notified', {
|
||||||
|
createdAt: '2019-08-30T19:33:48.651Z',
|
||||||
|
read: false,
|
||||||
|
reason: 'mentioned_in_comment',
|
||||||
|
}),
|
||||||
|
comment3.relateTo(neighbor, 'notified', {
|
||||||
|
createdAt: '2019-09-01T17:33:48.651Z',
|
||||||
|
read: false,
|
||||||
|
reason: 'mentioned_in_comment',
|
||||||
|
}),
|
||||||
|
])
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
describe('currentUser notifications', () => {
|
describe('notifications', () => {
|
||||||
const variables = {}
|
const notificationQuery = gql`
|
||||||
|
query($read: Boolean, $orderBy: NotificationOrdering) {
|
||||||
describe('authenticated', () => {
|
notifications(read: $read, orderBy: $orderBy) {
|
||||||
let headers
|
from {
|
||||||
beforeEach(async () => {
|
__typename
|
||||||
headers = await login({
|
... on Post {
|
||||||
email: 'test@example.org',
|
content
|
||||||
password: '1234',
|
}
|
||||||
})
|
... on Comment {
|
||||||
client = new GraphQLClient(host, {
|
content
|
||||||
headers,
|
}
|
||||||
})
|
}
|
||||||
})
|
read
|
||||||
|
createdAt
|
||||||
describe('given some notifications', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const neighborParams = {
|
|
||||||
email: 'neighbor@example.org',
|
|
||||||
password: '1234',
|
|
||||||
id: 'neighbor',
|
|
||||||
}
|
}
|
||||||
await Promise.all([
|
|
||||||
factory.create('User', neighborParams),
|
|
||||||
factory.create('Notification', {
|
|
||||||
id: 'post-mention-not-for-you',
|
|
||||||
reason: 'mentioned_in_post',
|
|
||||||
}),
|
|
||||||
factory.create('Notification', {
|
|
||||||
id: 'post-mention-already-seen',
|
|
||||||
read: true,
|
|
||||||
reason: 'mentioned_in_post',
|
|
||||||
}),
|
|
||||||
factory.create('Notification', {
|
|
||||||
id: 'post-mention-unseen',
|
|
||||||
reason: 'mentioned_in_post',
|
|
||||||
}),
|
|
||||||
factory.create('Notification', {
|
|
||||||
id: 'comment-mention-not-for-you',
|
|
||||||
reason: 'mentioned_in_comment',
|
|
||||||
}),
|
|
||||||
factory.create('Notification', {
|
|
||||||
id: 'comment-mention-already-seen',
|
|
||||||
read: true,
|
|
||||||
reason: 'mentioned_in_comment',
|
|
||||||
}),
|
|
||||||
factory.create('Notification', {
|
|
||||||
id: 'comment-mention-unseen',
|
|
||||||
reason: 'mentioned_in_comment',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
await factory.authenticateAs(neighborParams)
|
|
||||||
await factory.create('Post', { id: 'p1', categoryIds })
|
|
||||||
await Promise.all([
|
|
||||||
factory.relate('Notification', 'User', {
|
|
||||||
from: 'post-mention-not-for-you',
|
|
||||||
to: 'neighbor',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'Post', {
|
|
||||||
from: 'p1',
|
|
||||||
to: 'post-mention-not-for-you',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'User', {
|
|
||||||
from: 'post-mention-unseen',
|
|
||||||
to: 'you',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'Post', {
|
|
||||||
from: 'p1',
|
|
||||||
to: 'post-mention-unseen',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'User', {
|
|
||||||
from: 'post-mention-already-seen',
|
|
||||||
to: 'you',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'Post', {
|
|
||||||
from: 'p1',
|
|
||||||
to: 'post-mention-already-seen',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
// Comment and its notifications
|
|
||||||
await Promise.all([
|
|
||||||
factory.create('Comment', {
|
|
||||||
id: 'c1',
|
|
||||||
postId: 'p1',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
await Promise.all([
|
|
||||||
factory.relate('Notification', 'User', {
|
|
||||||
from: 'comment-mention-not-for-you',
|
|
||||||
to: 'neighbor',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'Comment', {
|
|
||||||
from: 'c1',
|
|
||||||
to: 'comment-mention-not-for-you',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'User', {
|
|
||||||
from: 'comment-mention-unseen',
|
|
||||||
to: 'you',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'Comment', {
|
|
||||||
from: 'c1',
|
|
||||||
to: 'comment-mention-unseen',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'User', {
|
|
||||||
from: 'comment-mention-already-seen',
|
|
||||||
to: 'you',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'Comment', {
|
|
||||||
from: 'c1',
|
|
||||||
to: 'comment-mention-already-seen',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('filter for read: false', () => {
|
|
||||||
const queryCurrentUserNotificationsFilterRead = gql`
|
|
||||||
query($read: Boolean) {
|
|
||||||
currentUser {
|
|
||||||
notifications(read: $read, orderBy: createdAt_desc) {
|
|
||||||
id
|
|
||||||
post {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
comment {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const variables = { read: false }
|
|
||||||
it('returns only unread notifications of current user', async () => {
|
|
||||||
const expected = {
|
|
||||||
currentUser: {
|
|
||||||
notifications: expect.arrayContaining([
|
|
||||||
{
|
|
||||||
id: 'post-mention-unseen',
|
|
||||||
post: {
|
|
||||||
id: 'p1',
|
|
||||||
},
|
|
||||||
comment: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'comment-mention-unseen',
|
|
||||||
post: null,
|
|
||||||
comment: {
|
|
||||||
id: 'c1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await expect(
|
|
||||||
client.request(queryCurrentUserNotificationsFilterRead, variables),
|
|
||||||
).resolves.toEqual(expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('no filters', () => {
|
|
||||||
const queryCurrentUserNotifications = gql`
|
|
||||||
query {
|
|
||||||
currentUser {
|
|
||||||
notifications(orderBy: createdAt_desc) {
|
|
||||||
id
|
|
||||||
post {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
comment {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
it('returns all notifications of current user', async () => {
|
|
||||||
const expected = {
|
|
||||||
currentUser: {
|
|
||||||
notifications: expect.arrayContaining([
|
|
||||||
{
|
|
||||||
id: 'post-mention-unseen',
|
|
||||||
post: {
|
|
||||||
id: 'p1',
|
|
||||||
},
|
|
||||||
comment: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'post-mention-already-seen',
|
|
||||||
post: {
|
|
||||||
id: 'p1',
|
|
||||||
},
|
|
||||||
comment: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'comment-mention-unseen',
|
|
||||||
comment: {
|
|
||||||
id: 'c1',
|
|
||||||
},
|
|
||||||
post: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'comment-mention-already-seen',
|
|
||||||
comment: {
|
|
||||||
id: 'c1',
|
|
||||||
},
|
|
||||||
post: null,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await expect(client.request(queryCurrentUserNotifications, variables)).resolves.toEqual(
|
|
||||||
expected,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('UpdateNotification', () => {
|
|
||||||
const mutationUpdateNotification = gql`
|
|
||||||
mutation($id: ID!, $read: Boolean) {
|
|
||||||
UpdateNotification(id: $id, read: $read) {
|
|
||||||
id
|
|
||||||
read
|
|
||||||
}
|
}
|
||||||
}
|
`
|
||||||
`
|
|
||||||
const variablesPostUpdateNotification = {
|
|
||||||
id: 'post-mention-to-be-updated',
|
|
||||||
read: true,
|
|
||||||
}
|
|
||||||
const variablesCommentUpdateNotification = {
|
|
||||||
id: 'comment-mention-to-be-updated',
|
|
||||||
read: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('given some notifications', () => {
|
|
||||||
let headers
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const mentionedParams = {
|
|
||||||
id: 'mentioned-1',
|
|
||||||
email: 'mentioned@example.org',
|
|
||||||
password: '1234',
|
|
||||||
slug: 'mentioned',
|
|
||||||
}
|
|
||||||
await Promise.all([
|
|
||||||
factory.create('User', mentionedParams),
|
|
||||||
factory.create('Notification', {
|
|
||||||
id: 'post-mention-to-be-updated',
|
|
||||||
reason: 'mentioned_in_post',
|
|
||||||
}),
|
|
||||||
factory.create('Notification', {
|
|
||||||
id: 'comment-mention-to-be-updated',
|
|
||||||
reason: 'mentioned_in_comment',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
await factory.authenticateAs(userParams)
|
|
||||||
await factory.create('Post', { id: 'p1', categoryIds })
|
|
||||||
await Promise.all([
|
|
||||||
factory.relate('Notification', 'User', {
|
|
||||||
from: 'post-mention-to-be-updated',
|
|
||||||
to: 'mentioned-1',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'Post', {
|
|
||||||
from: 'p1',
|
|
||||||
to: 'post-mention-to-be-updated',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
// Comment and its notifications
|
|
||||||
await Promise.all([
|
|
||||||
factory.create('Comment', {
|
|
||||||
id: 'c1',
|
|
||||||
postId: 'p1',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
await Promise.all([
|
|
||||||
factory.relate('Notification', 'User', {
|
|
||||||
from: 'comment-mention-to-be-updated',
|
|
||||||
to: 'mentioned-1',
|
|
||||||
}),
|
|
||||||
factory.relate('Notification', 'Comment', {
|
|
||||||
from: 'p1',
|
|
||||||
to: 'comment-mention-to-be-updated',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
client = new GraphQLClient(host)
|
const { errors } = await query({ query: notificationQuery })
|
||||||
await expect(
|
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
|
|
||||||
).rejects.toThrow('Not Authorised')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login({
|
authenticatedUser = await user.toJson()
|
||||||
email: 'test@example.org',
|
})
|
||||||
password: '1234',
|
|
||||||
})
|
describe('no filters', () => {
|
||||||
client = new GraphQLClient(host, {
|
it('returns all notifications of current user', async () => {
|
||||||
headers,
|
const expected = {
|
||||||
|
data: {
|
||||||
|
notifications: [
|
||||||
|
{
|
||||||
|
from: {
|
||||||
|
__typename: 'Comment',
|
||||||
|
content: 'You have seen this comment mentioning already',
|
||||||
|
},
|
||||||
|
read: true,
|
||||||
|
createdAt: '2019-08-30T15:33:48.651Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: {
|
||||||
|
__typename: 'Post',
|
||||||
|
content: 'Already seen post mention',
|
||||||
|
},
|
||||||
|
read: true,
|
||||||
|
createdAt: '2019-08-30T17:33:48.651Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: {
|
||||||
|
__typename: 'Comment',
|
||||||
|
content: 'You have been mentioned in a comment',
|
||||||
|
},
|
||||||
|
read: false,
|
||||||
|
createdAt: '2019-08-30T19:33:48.651Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: {
|
||||||
|
__typename: 'Post',
|
||||||
|
content: 'You have been mentioned in a post',
|
||||||
|
},
|
||||||
|
read: false,
|
||||||
|
createdAt: '2019-08-31T17:33:48.651Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(query({ query: notificationQuery, variables })).resolves.toMatchObject(
|
||||||
|
expected,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('filter for read: false', () => {
|
||||||
|
it('returns only unread notifications of current user', async () => {
|
||||||
|
const expected = expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
notifications: [
|
||||||
|
{
|
||||||
|
from: {
|
||||||
|
__typename: 'Comment',
|
||||||
|
content: 'You have been mentioned in a comment',
|
||||||
|
},
|
||||||
|
read: false,
|
||||||
|
createdAt: '2019-08-30T19:33:48.651Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: {
|
||||||
|
__typename: 'Post',
|
||||||
|
content: 'You have been mentioned in a post',
|
||||||
|
},
|
||||||
|
read: false,
|
||||||
|
createdAt: '2019-08-31T17:33:48.651Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
query({ query: notificationQuery, variables: { ...variables, read: false } }),
|
||||||
|
).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('if a resource gets deleted', () => {
|
||||||
|
const deletePostAction = async () => {
|
||||||
|
authenticatedUser = await author.toJson()
|
||||||
|
const deletePostMutation = gql`
|
||||||
|
mutation($id: ID!) {
|
||||||
|
DeletePost(id: $id) {
|
||||||
|
id
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: deletePostMutation, variables: { id: 'p3' } }),
|
||||||
|
).resolves.toMatchObject({ data: { DeletePost: { id: 'p3', deleted: true } } })
|
||||||
|
authenticatedUser = await user.toJson()
|
||||||
|
}
|
||||||
|
|
||||||
|
it('reduces notifications list', async () => {
|
||||||
|
await expect(
|
||||||
|
query({ query: notificationQuery, variables: { ...variables, read: false } }),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: { notifications: [expect.any(Object), expect.any(Object)] },
|
||||||
|
})
|
||||||
|
await deletePostAction()
|
||||||
|
await expect(
|
||||||
|
query({ query: notificationQuery, variables: { ...variables, read: false } }),
|
||||||
|
).resolves.toMatchObject({ data: { notifications: [] } })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('markAsRead', () => {
|
||||||
|
const markAsReadMutation = gql`
|
||||||
|
mutation($id: ID!) {
|
||||||
|
markAsRead(id: $id) {
|
||||||
|
from {
|
||||||
|
__typename
|
||||||
|
... on Post {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
await expect(
|
const result = await mutate({
|
||||||
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
|
mutation: markAsReadMutation,
|
||||||
).rejects.toThrow('Not Authorised')
|
variables: { ...variables, id: 'p1' },
|
||||||
|
})
|
||||||
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await user.toJson()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('and owner', () => {
|
describe('not being notified at all', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login({
|
variables = {
|
||||||
email: 'mentioned@example.org',
|
...variables,
|
||||||
password: '1234',
|
id: 'p1',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null', async () => {
|
||||||
|
const response = await mutate({ mutation: markAsReadMutation, variables })
|
||||||
|
expect(response.data.markAsRead).toEqual(null)
|
||||||
|
expect(response.errors).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('being notified', () => {
|
||||||
|
describe('on a post', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
variables = {
|
||||||
|
...variables,
|
||||||
|
id: 'p3',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
client = new GraphQLClient(host, {
|
|
||||||
headers,
|
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
|
||||||
|
const { data } = await mutate({ mutation: markAsReadMutation, variables })
|
||||||
|
expect(data).toEqual({
|
||||||
|
markAsRead: {
|
||||||
|
from: {
|
||||||
|
__typename: 'Post',
|
||||||
|
content: 'You have been mentioned in a post',
|
||||||
|
},
|
||||||
|
read: true,
|
||||||
|
createdAt: '2019-08-31T17:33:48.651Z',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('but notification was already marked as read', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
variables = {
|
||||||
|
...variables,
|
||||||
|
id: 'p2',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('returns null', async () => {
|
||||||
|
const response = await mutate({ mutation: markAsReadMutation, variables })
|
||||||
|
expect(response.data.markAsRead).toEqual(null)
|
||||||
|
expect(response.errors).toBeUndefined()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates post notification', async () => {
|
describe('on a comment', () => {
|
||||||
const expected = {
|
beforeEach(async () => {
|
||||||
UpdateNotification: {
|
variables = {
|
||||||
id: 'post-mention-to-be-updated',
|
...variables,
|
||||||
read: true,
|
id: 'c2',
|
||||||
},
|
}
|
||||||
}
|
})
|
||||||
await expect(
|
|
||||||
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
|
|
||||||
).resolves.toEqual(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('updates comment notification', async () => {
|
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
|
||||||
const expected = {
|
const { data } = await mutate({ mutation: markAsReadMutation, variables })
|
||||||
UpdateNotification: {
|
expect(data).toEqual({
|
||||||
id: 'comment-mention-to-be-updated',
|
markAsRead: {
|
||||||
read: true,
|
from: {
|
||||||
},
|
__typename: 'Comment',
|
||||||
}
|
content: 'You have been mentioned in a comment',
|
||||||
await expect(
|
},
|
||||||
client.request(mutationUpdateNotification, variablesCommentUpdateNotification),
|
read: true,
|
||||||
).resolves.toEqual(expected)
|
createdAt: '2019-08-30T19:33:48.651Z',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
|
|||||||
import fileUpload from './fileUpload'
|
import fileUpload from './fileUpload'
|
||||||
import { getBlockedUsers, getBlockedByUsers } from './users.js'
|
import { getBlockedUsers, getBlockedByUsers } from './users.js'
|
||||||
import { mergeWith, isArray } from 'lodash'
|
import { mergeWith, isArray } from 'lodash'
|
||||||
|
import Resolver from './helpers/Resolver'
|
||||||
|
|
||||||
const filterForBlockedUsers = async (params, context) => {
|
const filterForBlockedUsers = async (params, context) => {
|
||||||
if (!context.user) return params
|
if (!context.user) return params
|
||||||
@ -11,6 +12,8 @@ const filterForBlockedUsers = async (params, context) => {
|
|||||||
getBlockedByUsers(context),
|
getBlockedByUsers(context),
|
||||||
])
|
])
|
||||||
const badIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)]
|
const badIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)]
|
||||||
|
if (!badIds.length) return params
|
||||||
|
|
||||||
params.filter = mergeWith(
|
params.filter = mergeWith(
|
||||||
params.filter,
|
params.filter,
|
||||||
{
|
{
|
||||||
@ -70,6 +73,35 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
|
CreatePost: async (object, params, context, resolveInfo) => {
|
||||||
|
const { categoryIds } = params
|
||||||
|
delete params.categoryIds
|
||||||
|
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||||
|
params.id = params.id || uuid()
|
||||||
|
|
||||||
|
const createPostCypher = `CREATE (post:Post {params})
|
||||||
|
WITH post
|
||||||
|
MATCH (author:User {id: $userId})
|
||||||
|
MERGE (post)<-[:WROTE]-(author)
|
||||||
|
WITH post
|
||||||
|
UNWIND $categoryIds AS categoryId
|
||||||
|
MATCH (category:Category {id: categoryId})
|
||||||
|
MERGE (post)-[:CATEGORIZED]->(category)
|
||||||
|
RETURN post`
|
||||||
|
|
||||||
|
const createPostVariables = { userId: context.user.id, categoryIds, params }
|
||||||
|
|
||||||
|
const session = context.driver.session()
|
||||||
|
const transactionRes = await session.run(createPostCypher, createPostVariables)
|
||||||
|
|
||||||
|
const [post] = transactionRes.records.map(record => {
|
||||||
|
return record.get('post')
|
||||||
|
})
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
return post.properties
|
||||||
|
},
|
||||||
UpdatePost: async (object, params, context, resolveInfo) => {
|
UpdatePost: async (object, params, context, resolveInfo) => {
|
||||||
const { categoryIds } = params
|
const { categoryIds } = params
|
||||||
delete params.categoryIds
|
delete params.categoryIds
|
||||||
@ -109,34 +141,25 @@ export default {
|
|||||||
return post.properties
|
return post.properties
|
||||||
},
|
},
|
||||||
|
|
||||||
CreatePost: async (object, params, context, resolveInfo) => {
|
DeletePost: async (object, args, context, resolveInfo) => {
|
||||||
const { categoryIds } = params
|
|
||||||
delete params.categoryIds
|
|
||||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
|
||||||
params.id = params.id || uuid()
|
|
||||||
|
|
||||||
const createPostCypher = `CREATE (post:Post {params})
|
|
||||||
WITH post
|
|
||||||
MATCH (author:User {id: $userId})
|
|
||||||
MERGE (post)<-[:WROTE]-(author)
|
|
||||||
WITH post
|
|
||||||
UNWIND $categoryIds AS categoryId
|
|
||||||
MATCH (category:Category {id: categoryId})
|
|
||||||
MERGE (post)-[:CATEGORIZED]->(category)
|
|
||||||
RETURN post`
|
|
||||||
|
|
||||||
const createPostVariables = { userId: context.user.id, categoryIds, params }
|
|
||||||
|
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
const transactionRes = await session.run(createPostCypher, createPostVariables)
|
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||||
|
const transactionRes = await session.run(
|
||||||
const [post] = transactionRes.records.map(record => {
|
`
|
||||||
return record.get('post')
|
MATCH (post:Post {id: $postId})
|
||||||
})
|
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
||||||
|
SET post.deleted = TRUE
|
||||||
session.close()
|
SET post.content = 'UNAVAILABLE'
|
||||||
|
SET post.contentExcerpt = 'UNAVAILABLE'
|
||||||
return post.properties
|
SET post.title = 'UNAVAILABLE'
|
||||||
|
SET comment.deleted = TRUE
|
||||||
|
REMOVE post.image
|
||||||
|
RETURN post
|
||||||
|
`,
|
||||||
|
{ postId: args.id },
|
||||||
|
)
|
||||||
|
const [post] = transactionRes.records.map(record => record.get('post').properties)
|
||||||
|
return post
|
||||||
},
|
},
|
||||||
AddPostEmotions: async (object, params, context, resolveInfo) => {
|
AddPostEmotions: async (object, params, context, resolveInfo) => {
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
@ -179,4 +202,50 @@ export default {
|
|||||||
return emoted
|
return emoted
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Post: {
|
||||||
|
...Resolver('Post', {
|
||||||
|
undefinedToNull: ['activityId', 'objectId', 'image', 'language'],
|
||||||
|
hasMany: {
|
||||||
|
tags: '-[:TAGGED]->(related:Tag)',
|
||||||
|
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||||
|
comments: '<-[:COMMENTS]-(related:Comment)',
|
||||||
|
shoutedBy: '<-[:SHOUTED]-(related:User)',
|
||||||
|
emotions: '<-[related:EMOTED]',
|
||||||
|
},
|
||||||
|
hasOne: {
|
||||||
|
author: '<-[:WROTE]-(related:User)',
|
||||||
|
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
commentsCount:
|
||||||
|
'<-[:COMMENTS]-(related:Comment) WHERE NOT related.deleted = true AND NOT related.disabled = true',
|
||||||
|
shoutedCount:
|
||||||
|
'<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true',
|
||||||
|
emotionsCount: '<-[related:EMOTED]-(:User)',
|
||||||
|
},
|
||||||
|
boolean: {
|
||||||
|
shoutedByCurrentUser:
|
||||||
|
'MATCH(this)<-[:SHOUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
relatedContributions: async (parent, params, context, resolveInfo) => {
|
||||||
|
if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions
|
||||||
|
const { id } = parent
|
||||||
|
const statement = `
|
||||||
|
MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
||||||
|
WHERE NOT post.deleted AND NOT post.disabled
|
||||||
|
RETURN DISTINCT post
|
||||||
|
LIMIT 10
|
||||||
|
`
|
||||||
|
let relatedContributions
|
||||||
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
|
const result = await session.run(statement, { id })
|
||||||
|
relatedContributions = result.records.map(r => r.get('post').properties)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
return relatedContributions
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ describe('report', () => {
|
|||||||
let returnedObject
|
let returnedObject
|
||||||
let variables
|
let variables
|
||||||
let createPostVariables
|
let createPostVariables
|
||||||
|
let user
|
||||||
const categoryIds = ['cat9']
|
const categoryIds = ['cat9']
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -20,10 +21,10 @@ describe('report', () => {
|
|||||||
id: 'whatever',
|
id: 'whatever',
|
||||||
}
|
}
|
||||||
headers = {}
|
headers = {}
|
||||||
await factory.create('User', {
|
user = await factory.create('User', {
|
||||||
id: 'u1',
|
|
||||||
email: 'test@example.org',
|
email: 'test@example.org',
|
||||||
password: '1234',
|
password: '1234',
|
||||||
|
id: 'u1',
|
||||||
})
|
})
|
||||||
await factory.create('User', {
|
await factory.create('User', {
|
||||||
id: 'u2',
|
id: 'u2',
|
||||||
@ -127,11 +128,8 @@ describe('report', () => {
|
|||||||
|
|
||||||
describe('reported resource is a post', () => {
|
describe('reported resource is a post', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await factory.authenticateAs({
|
|
||||||
email: 'test@example.org',
|
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
await factory.create('Post', {
|
await factory.create('Post', {
|
||||||
|
author: user,
|
||||||
id: 'p23',
|
id: 'p23',
|
||||||
title: 'Matt and Robert having a pair-programming',
|
title: 'Matt and Robert having a pair-programming',
|
||||||
categoryIds,
|
categoryIds,
|
||||||
@ -182,12 +180,9 @@ describe('report', () => {
|
|||||||
content: 'please comment on me',
|
content: 'please comment on me',
|
||||||
categoryIds,
|
categoryIds,
|
||||||
}
|
}
|
||||||
const asAuthenticatedUser = await factory.authenticateAs({
|
await factory.create('Post', { ...createPostVariables, author: user })
|
||||||
email: 'test@example.org',
|
await factory.create('Comment', {
|
||||||
password: '1234',
|
author: user,
|
||||||
})
|
|
||||||
await asAuthenticatedUser.create('Post', createPostVariables)
|
|
||||||
await asAuthenticatedUser.create('Comment', {
|
|
||||||
postId: 'p1',
|
postId: 'p1',
|
||||||
id: 'c34',
|
id: 'c34',
|
||||||
content: 'Robert getting tired.',
|
content: 'Robert getting tired.',
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export default {
|
|||||||
SocialMedia: Resolver('SocialMedia', {
|
SocialMedia: Resolver('SocialMedia', {
|
||||||
idAttribute: 'url',
|
idAttribute: 'url',
|
||||||
hasOne: {
|
hasOne: {
|
||||||
ownedBy: '<-[:OWNED_BY]-(related:User)',
|
ownedBy: '-[:OWNED_BY]->(related:User)',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,11 +24,11 @@ export default {
|
|||||||
// }
|
// }
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
const result = await session.run(
|
const result = await session.run(
|
||||||
'MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})' +
|
`
|
||||||
'RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1',
|
MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})
|
||||||
{
|
RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1
|
||||||
userEmail: email,
|
`,
|
||||||
},
|
{ userEmail: email },
|
||||||
)
|
)
|
||||||
session.close()
|
session.close()
|
||||||
const [currentUser] = await result.records.map(record => {
|
const [currentUser] = await result.records.map(record => {
|
||||||
|
|||||||
@ -1,50 +1,48 @@
|
|||||||
import { GraphQLClient, request } from 'graphql-request'
|
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import CONFIG from './../../config'
|
import CONFIG from './../../config'
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import { host, login } from '../../jest/helpers'
|
import { gql } from '../../jest/helpers'
|
||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import createServer, { context } from '../../server'
|
||||||
|
import encode from '../../jwt/encode'
|
||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
|
let query
|
||||||
// here is the decoded JWT token:
|
let mutate
|
||||||
// {
|
let variables
|
||||||
// role: 'user',
|
let req
|
||||||
// locationName: null,
|
let user
|
||||||
// name: 'Jenny Rostock',
|
|
||||||
// about: null,
|
|
||||||
// avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
|
|
||||||
// id: 'u3',
|
|
||||||
// email: 'user@example.org',
|
|
||||||
// slug: 'jenny-rostock',
|
|
||||||
// iat: 1550846680,
|
|
||||||
// exp: 1637246680,
|
|
||||||
// aud: 'http://localhost:3000',
|
|
||||||
// iss: 'http://localhost:4000',
|
|
||||||
// sub: 'u3'
|
|
||||||
// }
|
|
||||||
const jennyRostocksHeaders = {
|
|
||||||
authorization:
|
|
||||||
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc',
|
|
||||||
}
|
|
||||||
|
|
||||||
const disable = async id => {
|
const disable = async id => {
|
||||||
const moderatorParams = { email: 'moderator@example.org', role: 'moderator', password: '1234' }
|
await factory.create('User', { id: 'u2', role: 'moderator' })
|
||||||
const asModerator = Factory()
|
const moderatorBearerToken = encode({ id: 'u2' })
|
||||||
await asModerator.create('User', moderatorParams)
|
req = { headers: { authorization: `Bearer ${moderatorBearerToken}` } }
|
||||||
await asModerator.authenticateAs(moderatorParams)
|
await mutate({
|
||||||
await asModerator.mutate('mutation($id: ID!) { disable(id: $id) }', { id })
|
mutation: gql`
|
||||||
|
mutation($id: ID!) {
|
||||||
|
disable(id: $id)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { id },
|
||||||
|
})
|
||||||
|
req = { headers: {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(() => {
|
||||||
await factory.create('User', {
|
user = null
|
||||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
req = { headers: {} }
|
||||||
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
|
})
|
||||||
name: 'Matilde Hermiston',
|
|
||||||
slug: 'matilde-hermiston',
|
beforeAll(() => {
|
||||||
role: 'user',
|
const { server } = createServer({
|
||||||
email: 'test@example.org',
|
context: () => {
|
||||||
password: '1234',
|
// One of the rare occasions where we test
|
||||||
|
// the actual `context` implementation here
|
||||||
|
return context({ req })
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
query = createTestClient(server).query
|
||||||
|
mutate = createTestClient(server).mutate
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@ -52,261 +50,269 @@ afterEach(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('isLoggedIn', () => {
|
describe('isLoggedIn', () => {
|
||||||
const query = '{ isLoggedIn }'
|
const isLoggedInQuery = gql`
|
||||||
|
{
|
||||||
|
isLoggedIn
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const respondsWith = async expected => {
|
||||||
|
await expect(query({ query: isLoggedInQuery })).resolves.toMatchObject(expected)
|
||||||
|
}
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('returns false', async () => {
|
it('returns false', async () => {
|
||||||
await expect(request(host, query)).resolves.toEqual({
|
await respondsWith({ data: { isLoggedIn: false } })
|
||||||
isLoggedIn: false,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with malformed JWT Bearer token', () => {
|
describe('authenticated', () => {
|
||||||
const headers = { authorization: 'blah' }
|
beforeEach(async () => {
|
||||||
const client = new GraphQLClient(host, { headers })
|
user = await factory.create('User', { id: 'u3' })
|
||||||
|
const userBearerToken = encode({ id: 'u3' })
|
||||||
it('returns false', async () => {
|
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
|
||||||
await expect(client.request(query)).resolves.toEqual({
|
|
||||||
isLoggedIn: false,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
describe('with valid JWT Bearer token', () => {
|
it('returns true', async () => {
|
||||||
const client = new GraphQLClient(host, { headers: jennyRostocksHeaders })
|
await respondsWith({ data: { isLoggedIn: true } })
|
||||||
|
})
|
||||||
|
|
||||||
it('returns false', async () => {
|
describe('but user is disabled', () => {
|
||||||
await expect(client.request(query)).resolves.toEqual({
|
beforeEach(async () => {
|
||||||
isLoggedIn: false,
|
await disable('u3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false', async () => {
|
||||||
|
await respondsWith({ data: { isLoggedIn: false } })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('and a corresponding user in the database', () => {
|
describe('but user is deleted', () => {
|
||||||
describe('user is enabled', () => {
|
beforeEach(async () => {
|
||||||
it('returns true', async () => {
|
await user.update({ updatedAt: new Date().toISOString(), deleted: true })
|
||||||
// see the decoded token above
|
|
||||||
await factory.create('User', { id: 'u3' })
|
|
||||||
await expect(client.request(query)).resolves.toEqual({
|
|
||||||
isLoggedIn: true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('user is disabled', () => {
|
it('returns false', async () => {
|
||||||
beforeEach(async () => {
|
await respondsWith({ data: { isLoggedIn: false } })
|
||||||
await factory.create('User', { id: 'u3' })
|
|
||||||
await disable('u3')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns false', async () => {
|
|
||||||
await expect(client.request(query)).resolves.toEqual({
|
|
||||||
isLoggedIn: false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('currentUser', () => {
|
describe('currentUser', () => {
|
||||||
const query = `{
|
const currentUserQuery = gql`
|
||||||
currentUser {
|
{
|
||||||
id
|
currentUser {
|
||||||
slug
|
id
|
||||||
name
|
slug
|
||||||
avatar
|
name
|
||||||
email
|
avatar
|
||||||
role
|
email
|
||||||
|
role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}`
|
`
|
||||||
|
|
||||||
|
const respondsWith = async expected => {
|
||||||
|
await expect(query({ query: currentUserQuery, variables })).resolves.toMatchObject(expected)
|
||||||
|
}
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('returns null', async () => {
|
it('returns null', async () => {
|
||||||
const expected = { currentUser: null }
|
await respondsWith({ data: { currentUser: null } })
|
||||||
await expect(request(host, query)).resolves.toEqual(expected)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with valid JWT Bearer Token', () => {
|
describe('authenticated', () => {
|
||||||
let client
|
|
||||||
let headers
|
|
||||||
|
|
||||||
describe('but no corresponding user in the database', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
client = new GraphQLClient(host, { headers: jennyRostocksHeaders })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns null', async () => {
|
|
||||||
const expected = { currentUser: null }
|
|
||||||
await expect(client.request(query)).resolves.toEqual(expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('and corresponding user in the database', () => {
|
describe('and corresponding user in the database', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
await factory.create('User', {
|
||||||
client = new GraphQLClient(host, { headers })
|
id: 'u3',
|
||||||
|
// the `id` is the only thing that has to match the decoded JWT bearer token
|
||||||
|
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
||||||
|
email: 'test@example.org',
|
||||||
|
name: 'Matilde Hermiston',
|
||||||
|
slug: 'matilde-hermiston',
|
||||||
|
role: 'user',
|
||||||
|
})
|
||||||
|
const userBearerToken = encode({ id: 'u3' })
|
||||||
|
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns the whole user object', async () => {
|
it('returns the whole user object', async () => {
|
||||||
const expected = {
|
const expected = {
|
||||||
currentUser: {
|
data: {
|
||||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
currentUser: {
|
||||||
email: 'test@example.org',
|
id: 'u3',
|
||||||
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
|
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
||||||
name: 'Matilde Hermiston',
|
email: 'test@example.org',
|
||||||
slug: 'matilde-hermiston',
|
name: 'Matilde Hermiston',
|
||||||
role: 'user',
|
slug: 'matilde-hermiston',
|
||||||
|
role: 'user',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await expect(client.request(query)).resolves.toEqual(expected)
|
await respondsWith(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('login', () => {
|
describe('login', () => {
|
||||||
const mutation = params => {
|
const loginMutation = gql`
|
||||||
const { email, password } = params
|
mutation($email: String!, $password: String!) {
|
||||||
return `
|
login(email: $email, password: $password)
|
||||||
mutation {
|
}
|
||||||
login(email:"${email}", password:"${password}")
|
`
|
||||||
}`
|
|
||||||
|
const respondsWith = async expected => {
|
||||||
|
await expect(mutate({ mutation: loginMutation, variables })).resolves.toMatchObject(expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
variables = { email: 'test@example.org', password: '1234' }
|
||||||
|
user = await factory.create('User', {
|
||||||
|
...variables,
|
||||||
|
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('ask for a `token`', () => {
|
describe('ask for a `token`', () => {
|
||||||
describe('with valid email/password combination', () => {
|
describe('with a valid email/password combination', () => {
|
||||||
it('responds with a JWT token', async () => {
|
it('responds with a JWT bearer token', async done => {
|
||||||
const data = await request(
|
const {
|
||||||
host,
|
data: { login: token },
|
||||||
mutation({
|
} = await mutate({ mutation: loginMutation, variables })
|
||||||
email: 'test@example.org',
|
|
||||||
password: '1234',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const token = data.login
|
|
||||||
jwt.verify(token, CONFIG.JWT_SECRET, (err, data) => {
|
jwt.verify(token, CONFIG.JWT_SECRET, (err, data) => {
|
||||||
expect(data.email).toEqual('test@example.org')
|
expect(data.email).toEqual('test@example.org')
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('but user account is deleted', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await user.update({ updatedAt: new Date().toISOString(), deleted: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('responds with "Incorrect email address or password."', async () => {
|
||||||
|
await respondsWith({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Incorrect email address or password.' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('but user account is disabled', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await disable('acb2d923-f3af-479e-9f00-61b12e864666')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('responds with "Your account has been disabled."', async () => {
|
||||||
|
await respondsWith({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Your account has been disabled.' }],
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('valid email/password but user is disabled', () => {
|
|
||||||
it('responds with "Your account has been disabled."', async () => {
|
|
||||||
await disable('acb2d923-f3af-479e-9f00-61b12e864666')
|
|
||||||
await expect(
|
|
||||||
request(
|
|
||||||
host,
|
|
||||||
mutation({
|
|
||||||
email: 'test@example.org',
|
|
||||||
password: '1234',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).rejects.toThrow('Your account has been disabled.')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('with a valid email but incorrect password', () => {
|
describe('with a valid email but incorrect password', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = { ...variables, email: 'test@example.org', password: 'wrong' }
|
||||||
|
})
|
||||||
|
|
||||||
it('responds with "Incorrect email address or password."', async () => {
|
it('responds with "Incorrect email address or password."', async () => {
|
||||||
await expect(
|
await respondsWith({
|
||||||
request(
|
errors: [{ message: 'Incorrect email address or password.' }],
|
||||||
host,
|
})
|
||||||
mutation({
|
|
||||||
email: 'test@example.org',
|
|
||||||
password: 'wrong',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).rejects.toThrow('Incorrect email address or password.')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with a non-existing email', () => {
|
describe('with a non-existing email', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = {
|
||||||
|
...variables,
|
||||||
|
email: 'non-existent@example.org',
|
||||||
|
password: '1234',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('responds with "Incorrect email address or password."', async () => {
|
it('responds with "Incorrect email address or password."', async () => {
|
||||||
await expect(
|
await respondsWith({
|
||||||
request(
|
errors: [{ message: 'Incorrect email address or password.' }],
|
||||||
host,
|
})
|
||||||
mutation({
|
|
||||||
email: 'non-existent@example.org',
|
|
||||||
password: 'wrong',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).rejects.toThrow('Incorrect email address or password.')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('change password', () => {
|
describe('change password', () => {
|
||||||
let headers
|
const changePasswordMutation = gql`
|
||||||
let client
|
mutation($oldPassword: String!, $newPassword: String!) {
|
||||||
|
changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
beforeEach(async () => {
|
const respondsWith = async expected => {
|
||||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
await expect(mutate({ mutation: changePasswordMutation, variables })).resolves.toMatchObject(
|
||||||
client = new GraphQLClient(host, { headers })
|
expected,
|
||||||
})
|
)
|
||||||
|
|
||||||
const mutation = params => {
|
|
||||||
const { oldPassword, newPassword } = params
|
|
||||||
return `
|
|
||||||
mutation {
|
|
||||||
changePassword(oldPassword:"${oldPassword}", newPassword:"${newPassword}")
|
|
||||||
}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('should be authenticated before changing password', () => {
|
beforeEach(async () => {
|
||||||
|
variables = { ...variables, oldPassword: 'what', newPassword: 'ever' }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
it('throws "Not Authorised!"', async () => {
|
it('throws "Not Authorised!"', async () => {
|
||||||
await expect(
|
await respondsWith({ errors: [{ message: 'Not Authorised!' }] })
|
||||||
request(
|
|
||||||
host,
|
|
||||||
mutation({
|
|
||||||
oldPassword: '1234',
|
|
||||||
newPassword: '1234',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).rejects.toThrow('Not Authorised!')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('old and new password should not match', () => {
|
describe('authenticated', () => {
|
||||||
it('responds with "Old password and new password should be different"', async () => {
|
beforeEach(async () => {
|
||||||
await expect(
|
await factory.create('User', { id: 'u3' })
|
||||||
client.request(
|
const userBearerToken = encode({ id: 'u3' })
|
||||||
mutation({
|
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
|
||||||
oldPassword: '1234',
|
|
||||||
newPassword: '1234',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).rejects.toThrow('Old password and new password should be different')
|
|
||||||
})
|
})
|
||||||
})
|
describe('old password === new password', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = { ...variables, oldPassword: '1234', newPassword: '1234' }
|
||||||
|
})
|
||||||
|
|
||||||
describe('incorrect old password', () => {
|
it('responds with "Old password and new password should be different"', async () => {
|
||||||
it('responds with "Old password isn\'t valid"', async () => {
|
await respondsWith({
|
||||||
await expect(
|
errors: [{ message: 'Old password and new password should be different' }],
|
||||||
client.request(
|
})
|
||||||
mutation({
|
})
|
||||||
oldPassword: 'notOldPassword',
|
|
||||||
newPassword: '12345',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).rejects.toThrow('Old password is not correct')
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
describe('correct password', () => {
|
describe('incorrect old password', () => {
|
||||||
it('changes the password if given correct credentials "', async () => {
|
beforeEach(() => {
|
||||||
const response = await client.request(
|
variables = {
|
||||||
mutation({
|
...variables,
|
||||||
|
oldPassword: 'notOldPassword',
|
||||||
|
newPassword: '12345',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('responds with "Old password isn\'t valid"', async () => {
|
||||||
|
await respondsWith({ errors: [{ message: 'Old password is not correct' }] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('correct password', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = {
|
||||||
|
...variables,
|
||||||
oldPassword: '1234',
|
oldPassword: '1234',
|
||||||
newPassword: '12345',
|
newPassword: '12345',
|
||||||
}),
|
}
|
||||||
)
|
})
|
||||||
await expect(response).toEqual(
|
|
||||||
expect.objectContaining({
|
it('changes the password if given correct credentials "', async () => {
|
||||||
changePassword: expect.any(String),
|
await respondsWith({ data: { changePassword: expect.any(String) } })
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -102,23 +102,49 @@ export default {
|
|||||||
const { resource } = params
|
const { resource } = params
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
|
|
||||||
if (resource && resource.length) {
|
let user
|
||||||
await Promise.all(
|
try {
|
||||||
resource.map(async node => {
|
if (resource && resource.length) {
|
||||||
await session.run(
|
await Promise.all(
|
||||||
`
|
resource.map(async node => {
|
||||||
|
await session.run(
|
||||||
|
`
|
||||||
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
|
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
|
||||||
|
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
|
||||||
SET resource.deleted = true
|
SET resource.deleted = true
|
||||||
|
SET resource.content = 'UNAVAILABLE'
|
||||||
|
SET resource.contentExcerpt = 'UNAVAILABLE'
|
||||||
|
SET comment.deleted = true
|
||||||
RETURN author`,
|
RETURN author`,
|
||||||
{
|
{
|
||||||
userId: context.user.id,
|
userId: context.user.id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||||
|
const transactionResult = await session.run(
|
||||||
|
`
|
||||||
|
MATCH (user:User {id: $userId})
|
||||||
|
SET user.deleted = true
|
||||||
|
SET user.name = 'UNAVAILABLE'
|
||||||
|
SET user.about = 'UNAVAILABLE'
|
||||||
|
WITH user
|
||||||
|
OPTIONAL MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress)
|
||||||
|
DETACH DELETE email
|
||||||
|
WITH user
|
||||||
|
OPTIONAL MATCH (user)<-[:OWNED_BY]-(socialMedia:SocialMedia)
|
||||||
|
DETACH DELETE socialMedia
|
||||||
|
RETURN user`,
|
||||||
|
{ userId: context.user.id },
|
||||||
)
|
)
|
||||||
|
user = transactionResult.records.map(r => r.get('user').properties)[0]
|
||||||
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
return user
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
@ -147,23 +173,27 @@ export default {
|
|||||||
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||||
},
|
},
|
||||||
count: {
|
count: {
|
||||||
contributionsCount: '-[:WROTE]->(related:Post)',
|
contributionsCount:
|
||||||
|
'-[:WROTE]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
|
||||||
friendsCount: '<-[:FRIENDS]->(related:User)',
|
friendsCount: '<-[:FRIENDS]->(related:User)',
|
||||||
followingCount: '-[:FOLLOWS]->(related:User)',
|
followingCount: '-[:FOLLOWS]->(related:User)',
|
||||||
followedByCount: '<-[:FOLLOWS]-(related:User)',
|
followedByCount: '<-[:FOLLOWS]-(related:User)',
|
||||||
commentedCount: '-[:WROTE]->(:Comment)-[:COMMENTS]->(related:Post)',
|
commentedCount:
|
||||||
shoutedCount: '-[:SHOUTED]->(related:Post)',
|
'-[:WROTE]->(c:Comment)-[:COMMENTS]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
|
||||||
|
shoutedCount:
|
||||||
|
'-[:SHOUTED]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
|
||||||
badgesCount: '<-[:REWARDED]-(related:Badge)',
|
badgesCount: '<-[:REWARDED]-(related:Badge)',
|
||||||
},
|
},
|
||||||
hasOne: {
|
hasOne: {
|
||||||
invitedBy: '<-[:INVITED]-(related:User)',
|
invitedBy: '<-[:INVITED]-(related:User)',
|
||||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||||
|
location: '-[:IS_IN]->(related:Location)',
|
||||||
},
|
},
|
||||||
hasMany: {
|
hasMany: {
|
||||||
followedBy: '<-[:FOLLOWS]-(related:User)',
|
followedBy: '<-[:FOLLOWS]-(related:User)',
|
||||||
following: '-[:FOLLOWS]->(related:User)',
|
following: '-[:FOLLOWS]->(related:User)',
|
||||||
friends: '-[:FRIENDS]-(related:User)',
|
friends: '-[:FRIENDS]-(related:User)',
|
||||||
socialMedia: '-[:OWNED_BY]->(related:SocialMedia',
|
socialMedia: '<-[:OWNED_BY]-(related:SocialMedia)',
|
||||||
contributions: '-[:WROTE]->(related:Post)',
|
contributions: '-[:WROTE]->(related:Post)',
|
||||||
comments: '-[:WROTE]->(related:Comment)',
|
comments: '-[:WROTE]->(related:Comment)',
|
||||||
shouted: '-[:SHOUTED]->(related:Post)',
|
shouted: '-[:SHOUTED]->(related:Post)',
|
||||||
|
|||||||
@ -1,274 +1,463 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import { host, login, gql } from '../../jest/helpers'
|
import { gql } from '../../jest/helpers'
|
||||||
import { neode } from '../../bootstrap/neo4j'
|
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||||
|
import createServer from '../../server'
|
||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|
||||||
let client
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
const instance = neode()
|
|
||||||
const categoryIds = ['cat9']
|
const categoryIds = ['cat9']
|
||||||
|
let user
|
||||||
|
|
||||||
|
let query
|
||||||
|
let mutate
|
||||||
|
let authenticatedUser
|
||||||
|
let variables
|
||||||
|
|
||||||
|
const driver = getDriver()
|
||||||
|
const neode = getNeode()
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
driver,
|
||||||
|
neode,
|
||||||
|
user: authenticatedUser,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
query = createTestClient(server).query
|
||||||
|
mutate = createTestClient(server).mutate
|
||||||
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await factory.cleanDatabase()
|
await factory.cleanDatabase()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('users', () => {
|
describe('User', () => {
|
||||||
describe('User', () => {
|
describe('query by email address', () => {
|
||||||
describe('query by email address', () => {
|
beforeEach(async () => {
|
||||||
beforeEach(async () => {
|
await factory.create('User', { name: 'Johnny', email: 'any-email-address@example.org' })
|
||||||
await factory.create('User', { name: 'Johnny', email: 'any-email-address@example.org' })
|
|
||||||
})
|
|
||||||
|
|
||||||
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 userParams = {
|
|
||||||
role: 'admin',
|
|
||||||
email: 'admin@example.org',
|
|
||||||
password: '1234',
|
|
||||||
}
|
|
||||||
const factory = Factory()
|
|
||||||
await factory.create('User', userParams)
|
|
||||||
const headers = await login(userParams)
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('is permitted', async () => {
|
|
||||||
await expect(client.request(query, variables)).resolves.toEqual({
|
|
||||||
User: [{ name: 'Johnny' }],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
describe('UpdateUser', () => {
|
const userQuery = gql`
|
||||||
const userParams = {
|
query($email: String) {
|
||||||
email: 'user@example.org',
|
User(email: $email) {
|
||||||
password: '1234',
|
|
||||||
id: 'u47',
|
|
||||||
name: 'John Doe',
|
|
||||||
}
|
|
||||||
const variables = {
|
|
||||||
id: 'u47',
|
|
||||||
name: 'John Doughnut',
|
|
||||||
}
|
|
||||||
|
|
||||||
const mutation = `
|
|
||||||
mutation($id: ID!, $name: String) {
|
|
||||||
UpdateUser(id: $id, name: $name) {
|
|
||||||
id
|
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
const variables = { email: 'any-email-address@example.org' }
|
||||||
|
|
||||||
beforeEach(async () => {
|
it('is forbidden', async () => {
|
||||||
await factory.create('User', userParams)
|
await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
|
||||||
|
errors: [{ message: 'Not Authorised!' }],
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('as another user', () => {
|
describe('as admin', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const someoneElseParams = {
|
const admin = await factory.create('User', {
|
||||||
email: 'someone-else@example.org',
|
role: 'admin',
|
||||||
password: '1234',
|
email: 'admin@example.org',
|
||||||
name: 'James Doe',
|
|
||||||
}
|
|
||||||
|
|
||||||
await factory.create('User', someoneElseParams)
|
|
||||||
const headers = await login(someoneElseParams)
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('is not allowed to change other user accounts', async () => {
|
|
||||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('as the same user', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const headers = await login(userParams)
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('name within specifications', async () => {
|
|
||||||
const expected = {
|
|
||||||
UpdateUser: {
|
|
||||||
id: 'u47',
|
|
||||||
name: 'John Doughnut',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('with `null` as name', async () => {
|
|
||||||
const variables = {
|
|
||||||
id: 'u47',
|
|
||||||
name: null,
|
|
||||||
}
|
|
||||||
const expected = '"name" must be a string'
|
|
||||||
await expect(client.request(mutation, variables)).rejects.toThrow(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('with too short name', async () => {
|
|
||||||
const variables = {
|
|
||||||
id: 'u47',
|
|
||||||
name: ' ',
|
|
||||||
}
|
|
||||||
const expected = '"name" length must be at least 3 characters long'
|
|
||||||
await expect(client.request(mutation, variables)).rejects.toThrow(expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('DeleteUser', () => {
|
|
||||||
let deleteUserVariables
|
|
||||||
let asAuthor
|
|
||||||
const deleteUserMutation = gql`
|
|
||||||
mutation($id: ID!, $resource: [Deletable]) {
|
|
||||||
DeleteUser(id: $id, resource: $resource) {
|
|
||||||
id
|
|
||||||
contributions {
|
|
||||||
id
|
|
||||||
deleted
|
|
||||||
}
|
|
||||||
comments {
|
|
||||||
id
|
|
||||||
deleted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
beforeEach(async () => {
|
|
||||||
await factory.create('User', {
|
|
||||||
email: 'test@example.org',
|
|
||||||
password: '1234',
|
|
||||||
id: 'u343',
|
|
||||||
})
|
|
||||||
await factory.create('User', {
|
|
||||||
email: 'friends-account@example.org',
|
|
||||||
password: '1234',
|
|
||||||
id: 'u565',
|
|
||||||
})
|
|
||||||
deleteUserVariables = { id: 'u343', resource: [] }
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
client = new GraphQLClient(host)
|
|
||||||
await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow(
|
|
||||||
'Not Authorised',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated', () => {
|
|
||||||
let headers
|
|
||||||
beforeEach(async () => {
|
|
||||||
headers = await login({
|
|
||||||
email: 'test@example.org',
|
|
||||||
password: '1234',
|
password: '1234',
|
||||||
})
|
})
|
||||||
client = new GraphQLClient(host, { headers })
|
authenticatedUser = await admin.toJson()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("attempting to delete another user's account", () => {
|
it('is permitted', async () => {
|
||||||
it('throws an authorization error', async () => {
|
await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
|
||||||
deleteUserVariables = { id: 'u565' }
|
data: { User: [{ name: 'Johnny' }] },
|
||||||
await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow(
|
})
|
||||||
'Not Authorised',
|
})
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('attempting to delete my own account', () => {
|
describe('UpdateUser', () => {
|
||||||
let expectedResponse
|
const userParams = {
|
||||||
beforeEach(async () => {
|
email: 'user@example.org',
|
||||||
asAuthor = Factory()
|
password: '1234',
|
||||||
await asAuthor.authenticateAs({
|
id: 'u47',
|
||||||
email: 'test@example.org',
|
name: 'John Doe',
|
||||||
password: '1234',
|
}
|
||||||
})
|
const variables = {
|
||||||
await instance.create('Category', {
|
id: 'u47',
|
||||||
id: 'cat9',
|
name: 'John Doughnut',
|
||||||
name: 'Democracy & Politics',
|
}
|
||||||
icon: 'university',
|
|
||||||
})
|
const updateUserMutation = gql`
|
||||||
await asAuthor.create('Post', {
|
mutation($id: ID!, $name: String) {
|
||||||
id: 'p139',
|
UpdateUser(id: $id, name: $name) {
|
||||||
content: 'Post by user u343',
|
id
|
||||||
categoryIds,
|
name
|
||||||
})
|
}
|
||||||
await asAuthor.create('Comment', {
|
}
|
||||||
id: 'c155',
|
`
|
||||||
postId: 'p139',
|
|
||||||
content: 'Comment by user u343',
|
beforeEach(async () => {
|
||||||
})
|
user = await factory.create('User', userParams)
|
||||||
expectedResponse = {
|
})
|
||||||
DeleteUser: {
|
|
||||||
id: 'u343',
|
describe('as another user', () => {
|
||||||
contributions: [{ id: 'p139', deleted: false }],
|
beforeEach(async () => {
|
||||||
comments: [{ id: 'c155', deleted: false }],
|
const someoneElseParams = {
|
||||||
},
|
email: 'someone-else@example.org',
|
||||||
}
|
password: '1234',
|
||||||
})
|
name: 'James Doe',
|
||||||
it("deletes my account, but doesn't delete posts or comments by default", async () => {
|
}
|
||||||
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
|
|
||||||
expectedResponse,
|
const someoneElse = await factory.create('User', someoneElseParams)
|
||||||
)
|
authenticatedUser = await someoneElse.toJson()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("deletes a user's", () => {
|
it('is not allowed to change other user accounts', async () => {
|
||||||
it('posts on request', async () => {
|
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||||
deleteUserVariables = { id: 'u343', resource: ['Post'] }
|
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
expectedResponse = {
|
})
|
||||||
DeleteUser: {
|
})
|
||||||
id: 'u343',
|
|
||||||
contributions: [{ id: 'p139', deleted: true }],
|
describe('as the same user', () => {
|
||||||
comments: [{ id: 'c155', deleted: false }],
|
beforeEach(async () => {
|
||||||
},
|
authenticatedUser = await user.toJson()
|
||||||
}
|
})
|
||||||
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
|
|
||||||
expectedResponse,
|
it('name within specifications', async () => {
|
||||||
)
|
const expected = {
|
||||||
})
|
data: {
|
||||||
|
UpdateUser: {
|
||||||
it('comments on request', async () => {
|
id: 'u47',
|
||||||
deleteUserVariables = { id: 'u343', resource: ['Comment'] }
|
name: 'John Doughnut',
|
||||||
expectedResponse = {
|
},
|
||||||
DeleteUser: {
|
},
|
||||||
id: 'u343',
|
}
|
||||||
contributions: [{ id: 'p139', deleted: false }],
|
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
|
||||||
comments: [{ id: 'c155', deleted: true }],
|
expected,
|
||||||
},
|
)
|
||||||
}
|
})
|
||||||
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
|
|
||||||
expectedResponse,
|
it('with `null` as name', async () => {
|
||||||
)
|
const variables = {
|
||||||
})
|
id: 'u47',
|
||||||
|
name: null,
|
||||||
it('posts and comments on request', async () => {
|
}
|
||||||
deleteUserVariables = { id: 'u343', resource: ['Post', 'Comment'] }
|
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||||
expectedResponse = {
|
expect(errors[0]).toHaveProperty(
|
||||||
DeleteUser: {
|
'message',
|
||||||
id: 'u343',
|
'child "name" fails because ["name" contains an invalid value, "name" must be a string]',
|
||||||
contributions: [{ id: 'p139', deleted: true }],
|
)
|
||||||
comments: [{ id: 'c155', deleted: true }],
|
})
|
||||||
},
|
|
||||||
}
|
it('with too short name', async () => {
|
||||||
await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual(
|
const variables = {
|
||||||
expectedResponse,
|
id: 'u47',
|
||||||
)
|
name: ' ',
|
||||||
})
|
}
|
||||||
|
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||||
|
expect(errors[0]).toHaveProperty(
|
||||||
|
'message',
|
||||||
|
'child "name" fails because ["name" length must be at least 3 characters long]',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('DeleteUser', () => {
|
||||||
|
const deleteUserMutation = gql`
|
||||||
|
mutation($id: ID!, $resource: [Deletable]) {
|
||||||
|
DeleteUser(id: $id, resource: $resource) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
about
|
||||||
|
deleted
|
||||||
|
contributions {
|
||||||
|
id
|
||||||
|
content
|
||||||
|
contentExcerpt
|
||||||
|
deleted
|
||||||
|
comments {
|
||||||
|
id
|
||||||
|
content
|
||||||
|
contentExcerpt
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
comments {
|
||||||
|
id
|
||||||
|
content
|
||||||
|
contentExcerpt
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
beforeEach(async () => {
|
||||||
|
variables = { id: ' u343', resource: [] }
|
||||||
|
|
||||||
|
user = await factory.create('User', {
|
||||||
|
name: 'My name should be deleted',
|
||||||
|
about: 'along with my about',
|
||||||
|
id: 'u343',
|
||||||
|
})
|
||||||
|
await factory.create('User', {
|
||||||
|
email: 'friends-account@example.org',
|
||||||
|
password: '1234',
|
||||||
|
id: 'not-my-account',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
const { errors } = await mutate({ mutation: deleteUserMutation, variables })
|
||||||
|
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await user.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("attempting to delete another user's account", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = { ...variables, id: 'not-my-account' }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an authorization error', async () => {
|
||||||
|
const { errors } = await mutate({ mutation: deleteUserMutation, variables })
|
||||||
|
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('attempting to delete my own account', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = { ...variables, id: 'u343' }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given posts and comments', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await factory.create('Category', {
|
||||||
|
id: 'cat9',
|
||||||
|
name: 'Democracy & Politics',
|
||||||
|
icon: 'university',
|
||||||
|
})
|
||||||
|
await factory.create('Post', {
|
||||||
|
author: user,
|
||||||
|
id: 'p139',
|
||||||
|
content: 'Post by user u343',
|
||||||
|
categoryIds,
|
||||||
|
})
|
||||||
|
await factory.create('Comment', {
|
||||||
|
author: user,
|
||||||
|
id: 'c155',
|
||||||
|
content: 'Comment by user u343',
|
||||||
|
})
|
||||||
|
await factory.create('Comment', {
|
||||||
|
postId: 'p139',
|
||||||
|
id: 'c156',
|
||||||
|
content: "A comment by someone else on user u343's post",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("deletes my account, but doesn't delete posts or comments by default", async () => {
|
||||||
|
const expectedResponse = {
|
||||||
|
data: {
|
||||||
|
DeleteUser: {
|
||||||
|
id: 'u343',
|
||||||
|
name: 'UNAVAILABLE',
|
||||||
|
about: 'UNAVAILABLE',
|
||||||
|
deleted: true,
|
||||||
|
contributions: [
|
||||||
|
{
|
||||||
|
id: 'p139',
|
||||||
|
content: 'Post by user u343',
|
||||||
|
contentExcerpt: 'Post by user u343',
|
||||||
|
deleted: false,
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: 'c156',
|
||||||
|
content: "A comment by someone else on user u343's post",
|
||||||
|
contentExcerpt: "A comment by someone else on user u343's post",
|
||||||
|
deleted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: 'c155',
|
||||||
|
content: 'Comment by user u343',
|
||||||
|
contentExcerpt: 'Comment by user u343',
|
||||||
|
deleted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(mutate({ mutation: deleteUserMutation, variables })).resolves.toMatchObject(
|
||||||
|
expectedResponse,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deletion of all post requested', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = { ...variables, resource: ['Post'] }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("marks user's posts as deleted", () => {
|
||||||
|
it('posts on request', async () => {
|
||||||
|
const expectedResponse = {
|
||||||
|
data: {
|
||||||
|
DeleteUser: {
|
||||||
|
id: 'u343',
|
||||||
|
name: 'UNAVAILABLE',
|
||||||
|
about: 'UNAVAILABLE',
|
||||||
|
deleted: true,
|
||||||
|
contributions: [
|
||||||
|
{
|
||||||
|
id: 'p139',
|
||||||
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
|
deleted: true,
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: 'c156',
|
||||||
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
|
deleted: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: 'c155',
|
||||||
|
content: 'Comment by user u343',
|
||||||
|
contentExcerpt: 'Comment by user u343',
|
||||||
|
deleted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: deleteUserMutation, variables }),
|
||||||
|
).resolves.toMatchObject(expectedResponse)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deletion of all comments requested', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = { ...variables, resource: ['Comment'] }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks comments as deleted', async () => {
|
||||||
|
const expectedResponse = {
|
||||||
|
data: {
|
||||||
|
DeleteUser: {
|
||||||
|
id: 'u343',
|
||||||
|
name: 'UNAVAILABLE',
|
||||||
|
about: 'UNAVAILABLE',
|
||||||
|
deleted: true,
|
||||||
|
contributions: [
|
||||||
|
{
|
||||||
|
id: 'p139',
|
||||||
|
content: 'Post by user u343',
|
||||||
|
contentExcerpt: 'Post by user u343',
|
||||||
|
deleted: false,
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: 'c156',
|
||||||
|
content: "A comment by someone else on user u343's post",
|
||||||
|
contentExcerpt: "A comment by someone else on user u343's post",
|
||||||
|
deleted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: 'c155',
|
||||||
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
|
deleted: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: deleteUserMutation, variables }),
|
||||||
|
).resolves.toMatchObject(expectedResponse)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deletion of all post and comments requested', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
variables = { ...variables, resource: ['Post', 'Comment'] }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks posts and comments as deleted', async () => {
|
||||||
|
const expectedResponse = {
|
||||||
|
data: {
|
||||||
|
DeleteUser: {
|
||||||
|
id: 'u343',
|
||||||
|
name: 'UNAVAILABLE',
|
||||||
|
about: 'UNAVAILABLE',
|
||||||
|
deleted: true,
|
||||||
|
contributions: [
|
||||||
|
{
|
||||||
|
id: 'p139',
|
||||||
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
|
deleted: true,
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: 'c156',
|
||||||
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
|
deleted: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: 'c155',
|
||||||
|
content: 'UNAVAILABLE',
|
||||||
|
contentExcerpt: 'UNAVAILABLE',
|
||||||
|
deleted: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: deleteUserMutation, variables }),
|
||||||
|
).resolves.toMatchObject(expectedResponse)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('connected `EmailAddress` nodes', () => {
|
||||||
|
it('will be removed completely', async () => {
|
||||||
|
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||||
|
await mutate({ mutation: deleteUserMutation, variables })
|
||||||
|
await expect(neode.all('EmailAddress')).resolves.toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('connected `SocialMedia` nodes', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const socialMedia = await factory.create('SocialMedia')
|
||||||
|
await socialMedia.relateTo(user, 'ownedBy')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('will be removed completely', async () => {
|
||||||
|
await expect(neode.all('SocialMedia')).resolves.toHaveLength(1)
|
||||||
|
await mutate({ mutation: deleteUserMutation, variables })
|
||||||
|
await expect(neode.all('SocialMedia')).resolves.toHaveLength(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
17
backend/src/schema/types/Location.gql
Normal file
17
backend/src/schema/types/Location.gql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
enum ReasonNotification {
|
enum ReasonNotification {
|
||||||
mentioned_in_post
|
mentioned_in_post
|
||||||
mentioned_in_comment
|
mentioned_in_comment
|
||||||
comment_on_post
|
commented_on_post
|
||||||
}
|
}
|
||||||
@ -4,7 +4,7 @@ type Query {
|
|||||||
currentUser: User
|
currentUser: User
|
||||||
# Get the latest Network Statistics
|
# Get the latest Network Statistics
|
||||||
statistics: Statistics!
|
statistics: Statistics!
|
||||||
findPosts(query: String!, limit: Int = 10): [Post]!
|
findPosts(query: String!, limit: Int = 10, filter: _PostFilter): [Post]!
|
||||||
@cypher(
|
@cypher(
|
||||||
statement: """
|
statement: """
|
||||||
CALL db.index.fulltext.queryNodes('full_text_search', $query)
|
CALL db.index.fulltext.queryNodes('full_text_search', $query)
|
||||||
@ -51,23 +51,6 @@ type Statistics {
|
|||||||
countShouts: Int!
|
countShouts: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Location {
|
|
||||||
id: ID!
|
|
||||||
name: String!
|
|
||||||
nameEN: String
|
|
||||||
nameDE: String
|
|
||||||
nameFR: String
|
|
||||||
nameNL: String
|
|
||||||
nameIT: String
|
|
||||||
nameES: String
|
|
||||||
namePT: String
|
|
||||||
namePL: String
|
|
||||||
type: String!
|
|
||||||
lat: Float
|
|
||||||
lng: Float
|
|
||||||
parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Report {
|
type Report {
|
||||||
id: ID!
|
id: ID!
|
||||||
submitter: User @relation(name: "REPORTED", direction: "IN")
|
submitter: User @relation(name: "REPORTED", direction: "IN")
|
||||||
|
|||||||
28
backend/src/schema/types/type/NOTIFIED.gql
Normal file
28
backend/src/schema/types/type/NOTIFIED.gql
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
type NOTIFIED {
|
||||||
|
from: NotificationSource
|
||||||
|
to: User
|
||||||
|
createdAt: String
|
||||||
|
read: Boolean
|
||||||
|
reason: NotificationReason
|
||||||
|
}
|
||||||
|
|
||||||
|
union NotificationSource = Post | Comment
|
||||||
|
|
||||||
|
enum NotificationOrdering {
|
||||||
|
createdAt_asc
|
||||||
|
createdAt_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NotificationReason {
|
||||||
|
mentioned_in_post
|
||||||
|
mentioned_in_comment
|
||||||
|
commented_on_post
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
notifications(read: Boolean, orderBy: NotificationOrdering): [NOTIFIED]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
markAsRead(id: ID!): NOTIFIED
|
||||||
|
}
|
||||||
@ -1,9 +0,0 @@
|
|||||||
type Notification {
|
|
||||||
id: ID!
|
|
||||||
read: Boolean
|
|
||||||
reason: ReasonNotification
|
|
||||||
createdAt: String
|
|
||||||
user: User @relation(name: "NOTIFIED", direction: "OUT")
|
|
||||||
post: Post @relation(name: "NOTIFIED", direction: "IN")
|
|
||||||
comment: Comment @relation(name: "NOTIFIED", direction: "IN")
|
|
||||||
}
|
|
||||||
@ -20,6 +20,7 @@ type Post {
|
|||||||
@cypher(
|
@cypher(
|
||||||
statement: """
|
statement: """
|
||||||
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
||||||
|
WHERE NOT post.deleted AND NOT post.disabled
|
||||||
RETURN DISTINCT post
|
RETURN DISTINCT post
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
"""
|
"""
|
||||||
@ -29,6 +30,11 @@ type Post {
|
|||||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
||||||
|
|
||||||
comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
|
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(DISTINCT r)"
|
||||||
|
)
|
||||||
|
|
||||||
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
|
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
|
||||||
shoutedCount: Int!
|
shoutedCount: Int!
|
||||||
@cypher(
|
@cypher(
|
||||||
@ -38,10 +44,7 @@ type Post {
|
|||||||
# Has the currently logged in user shouted that post?
|
# Has the currently logged in user shouted that post?
|
||||||
shoutedByCurrentUser: Boolean!
|
shoutedByCurrentUser: Boolean!
|
||||||
@cypher(
|
@cypher(
|
||||||
statement: """
|
statement: "MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1"
|
||||||
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
|
|
||||||
RETURN COUNT(u) >= 1
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
|
|
||||||
emotions: [EMOTED]
|
emotions: [EMOTED]
|
||||||
@ -52,26 +55,18 @@ type Post {
|
|||||||
type Mutation {
|
type Mutation {
|
||||||
CreatePost(
|
CreatePost(
|
||||||
id: ID
|
id: ID
|
||||||
activityId: String
|
|
||||||
objectId: String
|
|
||||||
title: String!
|
title: String!
|
||||||
slug: String
|
slug: String
|
||||||
content: String!
|
content: String!
|
||||||
image: String
|
image: String
|
||||||
imageUpload: Upload
|
imageUpload: Upload
|
||||||
visibility: Visibility
|
visibility: Visibility
|
||||||
deleted: Boolean
|
|
||||||
disabled: Boolean
|
|
||||||
createdAt: String
|
|
||||||
updatedAt: String
|
|
||||||
language: String
|
language: String
|
||||||
categoryIds: [ID]
|
categoryIds: [ID]
|
||||||
contentExcerpt: String
|
contentExcerpt: String
|
||||||
): Post
|
): Post
|
||||||
UpdatePost(
|
UpdatePost(
|
||||||
id: ID!
|
id: ID!
|
||||||
activityId: String
|
|
||||||
objectId: String
|
|
||||||
title: String!
|
title: String!
|
||||||
slug: String
|
slug: String
|
||||||
content: String!
|
content: String!
|
||||||
@ -79,10 +74,6 @@ type Mutation {
|
|||||||
image: String
|
image: String
|
||||||
imageUpload: Upload
|
imageUpload: Upload
|
||||||
visibility: Visibility
|
visibility: Visibility
|
||||||
deleted: Boolean
|
|
||||||
disabled: Boolean
|
|
||||||
createdAt: String
|
|
||||||
updatedAt: String
|
|
||||||
language: String
|
language: String
|
||||||
categoryIds: [ID]
|
categoryIds: [ID]
|
||||||
): Post
|
): Post
|
||||||
|
|||||||
@ -17,15 +17,13 @@ type User {
|
|||||||
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
||||||
locationName: String
|
locationName: String
|
||||||
about: String
|
about: String
|
||||||
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "OUT")
|
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
|
||||||
|
|
||||||
#createdAt: DateTime
|
#createdAt: DateTime
|
||||||
#updatedAt: DateTime
|
#updatedAt: DateTime
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
|
|
||||||
notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
|
|
||||||
|
|
||||||
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
|
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
|
||||||
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
|
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
|
||||||
|
|
||||||
@ -64,7 +62,7 @@ type User {
|
|||||||
)
|
)
|
||||||
|
|
||||||
comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
|
comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
|
||||||
commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment)-[:COMMENTS]->(p:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true AND NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))")
|
commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(:Comment)-[:COMMENTS]->(p:Post) WHERE NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))")
|
||||||
|
|
||||||
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
|
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)")
|
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
import uuid from 'uuid/v4'
|
import uuid from 'uuid/v4'
|
||||||
|
|
||||||
export default function(params) {
|
export default function create() {
|
||||||
const { id = uuid(), name, slug, icon } = params
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mutation: `
|
factory: async ({ args, neodeInstance }) => {
|
||||||
mutation($id: ID, $name: String!, $slug: String, $icon: String!) {
|
const defaults = {
|
||||||
CreateCategory(id: $id, name: $name, slug: $slug, icon: $icon) {
|
id: uuid(),
|
||||||
id
|
icon: 'img/badges/fundraisingbox_de_airship.svg',
|
||||||
name
|
name: 'Some category name',
|
||||||
}
|
}
|
||||||
}
|
args = {
|
||||||
`,
|
...defaults,
|
||||||
variables: { id, name, slug, icon },
|
...args,
|
||||||
|
}
|
||||||
|
return neodeInstance.create('Category', args)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,38 @@
|
|||||||
import faker from 'faker'
|
import faker from 'faker'
|
||||||
import uuid from 'uuid/v4'
|
import uuid from 'uuid/v4'
|
||||||
|
|
||||||
export default function(params) {
|
export default function create() {
|
||||||
const {
|
|
||||||
id = uuid(),
|
|
||||||
postId = 'p6',
|
|
||||||
content = [faker.lorem.sentence(), faker.lorem.sentence()].join('. '),
|
|
||||||
} = params
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mutation: `
|
factory: async ({ args, neodeInstance, factoryInstance }) => {
|
||||||
mutation($id: ID!, $postId: ID!, $content: String!) {
|
const defaults = {
|
||||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
id: uuid(),
|
||||||
id
|
content: [faker.lorem.sentence(), faker.lorem.sentence()].join('. '),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`,
|
args = {
|
||||||
variables: { id, postId, content },
|
...defaults,
|
||||||
|
...args,
|
||||||
|
}
|
||||||
|
args.contentExcerpt = args.contentExcerpt || args.content
|
||||||
|
|
||||||
|
let { post, postId } = args
|
||||||
|
delete args.post
|
||||||
|
delete args.postId
|
||||||
|
if (post && postId) throw new Error('You provided both post and postId')
|
||||||
|
if (postId) post = await neodeInstance.find('Post', postId)
|
||||||
|
post = post || (await factoryInstance.create('Post'))
|
||||||
|
|
||||||
|
let { author, authorId } = args
|
||||||
|
delete args.author
|
||||||
|
delete args.authorId
|
||||||
|
if (author && authorId) throw new Error('You provided both author and authorId')
|
||||||
|
if (authorId) author = await neodeInstance.find('User', authorId)
|
||||||
|
author = author || (await factoryInstance.create('User'))
|
||||||
|
|
||||||
|
delete args.author
|
||||||
|
const comment = await neodeInstance.create('Comment', args)
|
||||||
|
await comment.relateTo(post, 'post')
|
||||||
|
await comment.relateTo(author, 'author')
|
||||||
|
return comment
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,12 @@ import { GraphQLClient, request } from 'graphql-request'
|
|||||||
import { getDriver, neode } from '../../bootstrap/neo4j'
|
import { getDriver, neode } from '../../bootstrap/neo4j'
|
||||||
import createBadge from './badges.js'
|
import createBadge from './badges.js'
|
||||||
import createUser from './users.js'
|
import createUser from './users.js'
|
||||||
import createOrganization from './organizations.js'
|
|
||||||
import createPost from './posts.js'
|
import createPost from './posts.js'
|
||||||
import createComment from './comments.js'
|
import createComment from './comments.js'
|
||||||
import createCategory from './categories.js'
|
import createCategory from './categories.js'
|
||||||
import createTag from './tags.js'
|
import createTag from './tags.js'
|
||||||
import createReport from './reports.js'
|
import createSocialMedia from './socialMedia.js'
|
||||||
import createNotification from './notifications.js'
|
import createLocation from './locations.js'
|
||||||
|
|
||||||
export const seedServerHost = 'http://127.0.0.1:4001'
|
export const seedServerHost = 'http://127.0.0.1:4001'
|
||||||
|
|
||||||
@ -25,13 +24,12 @@ const authenticatedHeaders = async ({ email, password }, host) => {
|
|||||||
const factories = {
|
const factories = {
|
||||||
Badge: createBadge,
|
Badge: createBadge,
|
||||||
User: createUser,
|
User: createUser,
|
||||||
Organization: createOrganization,
|
|
||||||
Post: createPost,
|
Post: createPost,
|
||||||
Comment: createComment,
|
Comment: createComment,
|
||||||
Category: createCategory,
|
Category: createCategory,
|
||||||
Tag: createTag,
|
Tag: createTag,
|
||||||
Report: createReport,
|
SocialMedia: createSocialMedia,
|
||||||
Notification: createNotification,
|
Location: createLocation,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cleanDatabase = async (options = {}) => {
|
export const cleanDatabase = async (options = {}) => {
|
||||||
@ -81,6 +79,7 @@ export default function Factory(options = {}) {
|
|||||||
this.lastResponse = await factory({
|
this.lastResponse = await factory({
|
||||||
args,
|
args,
|
||||||
neodeInstance,
|
neodeInstance,
|
||||||
|
factoryInstance: this,
|
||||||
})
|
})
|
||||||
return this.lastResponse
|
return this.lastResponse
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
24
backend/src/seed/factories/locations.js
Normal file
24
backend/src/seed/factories/locations.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export default function create() {
|
||||||
|
return {
|
||||||
|
factory: async ({ args, neodeInstance }) => {
|
||||||
|
const defaults = {
|
||||||
|
name: 'Germany',
|
||||||
|
namePT: 'Alemanha',
|
||||||
|
nameDE: 'Deutschland',
|
||||||
|
nameES: 'Alemania',
|
||||||
|
nameNL: 'Duitsland',
|
||||||
|
namePL: 'Niemcy',
|
||||||
|
nameFR: 'Allemagne',
|
||||||
|
nameIT: 'Germania',
|
||||||
|
nameEN: 'Germany',
|
||||||
|
id: 'country.10743216036480410',
|
||||||
|
type: 'country',
|
||||||
|
}
|
||||||
|
args = {
|
||||||
|
...defaults,
|
||||||
|
...args,
|
||||||
|
}
|
||||||
|
return neodeInstance.create('Location', args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import uuid from 'uuid/v4'
|
|
||||||
|
|
||||||
export default function(params) {
|
|
||||||
const { id = uuid(), read = false } = params
|
|
||||||
|
|
||||||
return {
|
|
||||||
mutation: `
|
|
||||||
mutation($id: ID, $read: Boolean) {
|
|
||||||
CreateNotification(id: $id, read: $read) {
|
|
||||||
id
|
|
||||||
read
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables: { id, read },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import faker from 'faker'
|
|
||||||
import uuid from 'uuid/v4'
|
|
||||||
|
|
||||||
export default function create(params) {
|
|
||||||
const {
|
|
||||||
id = uuid(),
|
|
||||||
name = faker.company.companyName(),
|
|
||||||
description = faker.company.catchPhrase(),
|
|
||||||
} = params
|
|
||||||
|
|
||||||
return {
|
|
||||||
mutation: `
|
|
||||||
mutation($id: ID!, $name: String!, $description: String!) {
|
|
||||||
CreateOrganization(id: $id, name: $name, description: $description) {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables: { id, name, description },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +1,60 @@
|
|||||||
import faker from 'faker'
|
import faker from 'faker'
|
||||||
|
import slugify from 'slug'
|
||||||
import uuid from 'uuid/v4'
|
import uuid from 'uuid/v4'
|
||||||
|
|
||||||
export default function(params) {
|
export default function create() {
|
||||||
const {
|
|
||||||
id = uuid(),
|
|
||||||
slug = '',
|
|
||||||
title = faker.lorem.sentence(),
|
|
||||||
content = [
|
|
||||||
faker.lorem.sentence(),
|
|
||||||
faker.lorem.sentence(),
|
|
||||||
faker.lorem.sentence(),
|
|
||||||
faker.lorem.sentence(),
|
|
||||||
faker.lorem.sentence(),
|
|
||||||
].join('. '),
|
|
||||||
image = faker.image.unsplash.imageUrl(),
|
|
||||||
visibility = 'public',
|
|
||||||
deleted = false,
|
|
||||||
categoryIds,
|
|
||||||
} = params
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mutation: `
|
factory: async ({ args, neodeInstance, factoryInstance }) => {
|
||||||
mutation(
|
const defaults = {
|
||||||
$id: ID!
|
id: uuid(),
|
||||||
$slug: String
|
title: faker.lorem.sentence(),
|
||||||
$title: String!
|
content: [
|
||||||
$content: String!
|
faker.lorem.sentence(),
|
||||||
$image: String
|
faker.lorem.sentence(),
|
||||||
$visibility: Visibility
|
faker.lorem.sentence(),
|
||||||
$deleted: Boolean
|
faker.lorem.sentence(),
|
||||||
$categoryIds: [ID]
|
faker.lorem.sentence(),
|
||||||
) {
|
].join('. '),
|
||||||
CreatePost(
|
image: faker.image.unsplash.imageUrl(),
|
||||||
id: $id
|
visibility: 'public',
|
||||||
slug: $slug
|
deleted: false,
|
||||||
title: $title
|
categoryIds: [],
|
||||||
content: $content
|
|
||||||
image: $image
|
|
||||||
visibility: $visibility
|
|
||||||
deleted: $deleted
|
|
||||||
categoryIds: $categoryIds
|
|
||||||
) {
|
|
||||||
title
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`,
|
args = {
|
||||||
variables: { id, slug, title, content, image, visibility, deleted, categoryIds },
|
...defaults,
|
||||||
|
...args,
|
||||||
|
}
|
||||||
|
args.slug = args.slug || slugify(args.title, { lower: true })
|
||||||
|
args.contentExcerpt = args.contentExcerpt || args.content
|
||||||
|
|
||||||
|
let { categories, categoryIds } = args
|
||||||
|
delete args.categories
|
||||||
|
delete args.categoryIds
|
||||||
|
if (categories && categoryIds) throw new Error('You provided both category and categoryIds')
|
||||||
|
if (categoryIds)
|
||||||
|
categories = await Promise.all(categoryIds.map(id => neodeInstance.find('Category', id)))
|
||||||
|
categories = categories || (await Promise.all([factoryInstance.create('Category')]))
|
||||||
|
|
||||||
|
const { tagIds = [] } = args
|
||||||
|
delete args.tags
|
||||||
|
const tags = await Promise.all(
|
||||||
|
tagIds.map(t => {
|
||||||
|
return neodeInstance.find('Tag', t)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
let { author, authorId } = args
|
||||||
|
delete args.author
|
||||||
|
delete args.authorId
|
||||||
|
if (author && authorId) throw new Error('You provided both author and authorId')
|
||||||
|
if (authorId) author = await neodeInstance.find('User', authorId)
|
||||||
|
author = author || (await factoryInstance.create('User'))
|
||||||
|
|
||||||
|
const post = await neodeInstance.create('Post', args)
|
||||||
|
await post.relateTo(author, 'author')
|
||||||
|
await Promise.all(categories.map(c => c.relateTo(post, 'post')))
|
||||||
|
await Promise.all(tags.map(t => t.relateTo(post, 'post')))
|
||||||
|
return post
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
import faker from 'faker'
|
|
||||||
|
|
||||||
export default function create(params) {
|
|
||||||
const { description = faker.lorem.sentence(), id } = params
|
|
||||||
|
|
||||||
return {
|
|
||||||
mutation: `
|
|
||||||
mutation($id: ID!, $description: String!) {
|
|
||||||
report(description: $description, id: $id) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables: {
|
|
||||||
id,
|
|
||||||
description,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
backend/src/seed/factories/socialMedia.js
Normal file
14
backend/src/seed/factories/socialMedia.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export default function create() {
|
||||||
|
return {
|
||||||
|
factory: async ({ args, neodeInstance }) => {
|
||||||
|
const defaults = {
|
||||||
|
url: 'https://mastodon.social/@Gargron',
|
||||||
|
}
|
||||||
|
args = {
|
||||||
|
...defaults,
|
||||||
|
...args,
|
||||||
|
}
|
||||||
|
return neodeInstance.create('SocialMedia', args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +1,12 @@
|
|||||||
import uuid from 'uuid/v4'
|
export default function create() {
|
||||||
|
|
||||||
export default function(params) {
|
|
||||||
const { id = uuid(), name = '#human-connection' } = params
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mutation: `
|
factory: async ({ args, neodeInstance }) => {
|
||||||
mutation($id: ID!) {
|
const defaults = { name: '#human-connection' }
|
||||||
CreateTag(id: $id) {
|
args = {
|
||||||
id
|
...defaults,
|
||||||
}
|
...args,
|
||||||
}
|
}
|
||||||
`,
|
return neodeInstance.create('Tag', args)
|
||||||
variables: { id, name },
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,110 @@
|
|||||||
import faker from 'faker'
|
import faker from 'faker'
|
||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import createServer from '../server'
|
||||||
import Factory from './factories'
|
import Factory from './factories'
|
||||||
|
import { neode as getNeode, getDriver } from '../bootstrap/neo4j'
|
||||||
|
import { gql } from '../jest/helpers'
|
||||||
|
|
||||||
/* eslint-disable no-multi-spaces */
|
/* eslint-disable no-multi-spaces */
|
||||||
;(async function() {
|
;(async function() {
|
||||||
|
let authenticatedUser = null
|
||||||
|
const driver = getDriver()
|
||||||
|
const factory = Factory()
|
||||||
|
const neode = getNeode()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
driver,
|
||||||
|
neode,
|
||||||
|
user: authenticatedUser,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { mutate } = createTestClient(server)
|
||||||
|
|
||||||
const f = Factory()
|
const f = Factory()
|
||||||
|
|
||||||
|
const [Hamburg, Berlin, Germany, Paris, France] = await Promise.all([
|
||||||
|
f.create('Location', {
|
||||||
|
id: 'region.5127278006398860',
|
||||||
|
name: 'Hamburg',
|
||||||
|
type: 'region',
|
||||||
|
lat: 10.0,
|
||||||
|
lng: 53.55,
|
||||||
|
nameES: 'Hamburgo',
|
||||||
|
nameFR: 'Hambourg',
|
||||||
|
nameIT: 'Amburgo',
|
||||||
|
nameEN: 'Hamburg',
|
||||||
|
namePT: 'Hamburgo',
|
||||||
|
nameDE: 'Hamburg',
|
||||||
|
nameNL: 'Hamburg',
|
||||||
|
namePL: 'Hamburg',
|
||||||
|
}),
|
||||||
|
f.create('Location', {
|
||||||
|
id: 'region.14880313158564380',
|
||||||
|
type: 'region',
|
||||||
|
name: 'Berlin',
|
||||||
|
lat: 13.38333,
|
||||||
|
lng: 52.51667,
|
||||||
|
nameES: 'Berlín',
|
||||||
|
nameFR: 'Berlin',
|
||||||
|
nameIT: 'Berlino',
|
||||||
|
nameEN: 'Berlin',
|
||||||
|
namePT: 'Berlim',
|
||||||
|
nameDE: 'Berlin',
|
||||||
|
nameNL: 'Berlijn',
|
||||||
|
namePL: 'Berlin',
|
||||||
|
}),
|
||||||
|
f.create('Location', {
|
||||||
|
id: 'country.10743216036480410',
|
||||||
|
name: 'Germany',
|
||||||
|
type: 'country',
|
||||||
|
namePT: 'Alemanha',
|
||||||
|
nameDE: 'Deutschland',
|
||||||
|
nameES: 'Alemania',
|
||||||
|
nameNL: 'Duitsland',
|
||||||
|
namePL: 'Niemcy',
|
||||||
|
nameFR: 'Allemagne',
|
||||||
|
nameIT: 'Germania',
|
||||||
|
nameEN: 'Germany',
|
||||||
|
}),
|
||||||
|
f.create('Location', {
|
||||||
|
id: 'region.9397217726497330',
|
||||||
|
name: 'Paris',
|
||||||
|
type: 'region',
|
||||||
|
lat: 2.35183,
|
||||||
|
lng: 48.85658,
|
||||||
|
nameES: 'París',
|
||||||
|
nameFR: 'Paris',
|
||||||
|
nameIT: 'Parigi',
|
||||||
|
nameEN: 'Paris',
|
||||||
|
namePT: 'Paris',
|
||||||
|
nameDE: 'Paris',
|
||||||
|
nameNL: 'Parijs',
|
||||||
|
namePL: 'Paryż',
|
||||||
|
}),
|
||||||
|
f.create('Location', {
|
||||||
|
id: 'country.9759535382641660',
|
||||||
|
name: 'France',
|
||||||
|
type: 'country',
|
||||||
|
namePT: 'França',
|
||||||
|
nameDE: 'Frankreich',
|
||||||
|
nameES: 'Francia',
|
||||||
|
nameNL: 'Frankrijk',
|
||||||
|
namePL: 'Francja',
|
||||||
|
nameFR: 'France',
|
||||||
|
nameIT: 'Francia',
|
||||||
|
nameEN: 'France',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
await Promise.all([
|
||||||
|
Berlin.relateTo(Germany, 'isIn'),
|
||||||
|
Hamburg.relateTo(Germany, 'isIn'),
|
||||||
|
Paris.relateTo(France, 'isIn'),
|
||||||
|
])
|
||||||
|
|
||||||
const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([
|
const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([
|
||||||
f.create('Badge', {
|
f.create('Badge', {
|
||||||
id: 'indiegogo_en_racoon',
|
id: 'indiegogo_en_racoon',
|
||||||
@ -36,9 +136,9 @@ import Factory from './factories'
|
|||||||
peterLustig,
|
peterLustig,
|
||||||
bobDerBaumeister,
|
bobDerBaumeister,
|
||||||
jennyRostock,
|
jennyRostock,
|
||||||
tick, // eslint-disable-line no-unused-vars
|
huey,
|
||||||
trick, // eslint-disable-line no-unused-vars
|
dewey,
|
||||||
track, // eslint-disable-line no-unused-vars
|
louie,
|
||||||
dagobert,
|
dagobert,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
f.create('User', {
|
f.create('User', {
|
||||||
@ -64,22 +164,22 @@ import Factory from './factories'
|
|||||||
}),
|
}),
|
||||||
f.create('User', {
|
f.create('User', {
|
||||||
id: 'u4',
|
id: 'u4',
|
||||||
name: 'Huey (Tick)',
|
name: 'Huey',
|
||||||
slug: 'huey-tick',
|
slug: 'huey',
|
||||||
role: 'user',
|
role: 'user',
|
||||||
email: 'huey@example.org',
|
email: 'huey@example.org',
|
||||||
}),
|
}),
|
||||||
f.create('User', {
|
f.create('User', {
|
||||||
id: 'u5',
|
id: 'u5',
|
||||||
name: 'Dewey (Trick)',
|
name: 'Dewey',
|
||||||
slug: 'dewey-trick',
|
slug: 'dewey',
|
||||||
role: 'user',
|
role: 'user',
|
||||||
email: 'dewey@example.org',
|
email: 'dewey@example.org',
|
||||||
}),
|
}),
|
||||||
f.create('User', {
|
f.create('User', {
|
||||||
id: 'u6',
|
id: 'u6',
|
||||||
name: 'Louie (Track)',
|
name: 'Louie',
|
||||||
slug: 'louie-track',
|
slug: 'louie',
|
||||||
role: 'user',
|
role: 'user',
|
||||||
email: 'louie@example.org',
|
email: 'louie@example.org',
|
||||||
}),
|
}),
|
||||||
@ -92,31 +192,11 @@ import Factory from './factories'
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const [asAdmin, asModerator, asUser, asTick, asTrick, asTrack] = await Promise.all([
|
await Promise.all([
|
||||||
Factory().authenticateAs({
|
peterLustig.relateTo(Berlin, 'isIn'),
|
||||||
email: 'admin@example.org',
|
bobDerBaumeister.relateTo(Hamburg, 'isIn'),
|
||||||
password: '1234',
|
jennyRostock.relateTo(Paris, 'isIn'),
|
||||||
}),
|
huey.relateTo(Paris, 'isIn'),
|
||||||
Factory().authenticateAs({
|
|
||||||
email: 'moderator@example.org',
|
|
||||||
password: '1234',
|
|
||||||
}),
|
|
||||||
Factory().authenticateAs({
|
|
||||||
email: 'user@example.org',
|
|
||||||
password: '1234',
|
|
||||||
}),
|
|
||||||
Factory().authenticateAs({
|
|
||||||
email: 'huey@example.org',
|
|
||||||
password: '1234',
|
|
||||||
}),
|
|
||||||
Factory().authenticateAs({
|
|
||||||
email: 'dewey@example.org',
|
|
||||||
password: '1234',
|
|
||||||
}),
|
|
||||||
Factory().authenticateAs({
|
|
||||||
email: 'louie@example.org',
|
|
||||||
password: '1234',
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -133,16 +213,16 @@ import Factory from './factories'
|
|||||||
bobDerBaumeister.relateTo(jennyRostock, 'friends'),
|
bobDerBaumeister.relateTo(jennyRostock, 'friends'),
|
||||||
|
|
||||||
peterLustig.relateTo(jennyRostock, 'following'),
|
peterLustig.relateTo(jennyRostock, 'following'),
|
||||||
peterLustig.relateTo(tick, 'following'),
|
peterLustig.relateTo(huey, 'following'),
|
||||||
bobDerBaumeister.relateTo(tick, 'following'),
|
bobDerBaumeister.relateTo(huey, 'following'),
|
||||||
jennyRostock.relateTo(tick, 'following'),
|
jennyRostock.relateTo(huey, 'following'),
|
||||||
tick.relateTo(track, 'following'),
|
huey.relateTo(dewey, 'following'),
|
||||||
trick.relateTo(tick, 'following'),
|
dewey.relateTo(huey, 'following'),
|
||||||
track.relateTo(jennyRostock, 'following'),
|
louie.relateTo(jennyRostock, 'following'),
|
||||||
|
|
||||||
dagobert.relateTo(tick, 'blocked'),
|
dagobert.relateTo(huey, 'blocked'),
|
||||||
dagobert.relateTo(trick, 'blocked'),
|
dagobert.relateTo(dewey, 'blocked'),
|
||||||
dagobert.relateTo(track, 'blocked'),
|
dagobert.relateTo(louie, 'blocked'),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -244,25 +324,90 @@ import Factory from './factories'
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
const [environment, nature, democracy, freedom] = await Promise.all([
|
||||||
f.create('Tag', {
|
f.create('Tag', {
|
||||||
id: 'Umwelt',
|
id: 'Environment',
|
||||||
name: 'Umwelt',
|
|
||||||
}),
|
}),
|
||||||
f.create('Tag', {
|
f.create('Tag', {
|
||||||
id: 'Naturschutz',
|
id: 'Nature',
|
||||||
name: 'Naturschutz',
|
|
||||||
}),
|
}),
|
||||||
f.create('Tag', {
|
f.create('Tag', {
|
||||||
id: 'Demokratie',
|
id: 'Democracy',
|
||||||
name: 'Demokratie',
|
|
||||||
}),
|
}),
|
||||||
f.create('Tag', {
|
f.create('Tag', {
|
||||||
id: 'Freiheit',
|
id: 'Freedom',
|
||||||
name: 'Freiheit',
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const [p0, p1, p3, p4, p5, p6, p9, p10, p11, p13, p14, p15] = await Promise.all([
|
||||||
|
factory.create('Post', {
|
||||||
|
author: peterLustig,
|
||||||
|
id: 'p0',
|
||||||
|
image: faker.image.unsplash.food(),
|
||||||
|
categoryIds: ['cat16'],
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author: bobDerBaumeister,
|
||||||
|
id: 'p1',
|
||||||
|
image: faker.image.unsplash.technology(),
|
||||||
|
categoryIds: ['cat1'],
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author: huey,
|
||||||
|
id: 'p3',
|
||||||
|
categoryIds: ['cat3'],
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author: dewey,
|
||||||
|
id: 'p4',
|
||||||
|
categoryIds: ['cat4'],
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author: louie,
|
||||||
|
id: 'p5',
|
||||||
|
categoryIds: ['cat5'],
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
authorId: 'u1',
|
||||||
|
id: 'p6',
|
||||||
|
image: faker.image.unsplash.buildings(),
|
||||||
|
categoryIds: ['cat6'],
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author: huey,
|
||||||
|
id: 'p9',
|
||||||
|
categoryIds: ['cat9'],
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author: dewey,
|
||||||
|
id: 'p10',
|
||||||
|
categoryIds: ['cat10'],
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author: louie,
|
||||||
|
id: 'p11',
|
||||||
|
image: faker.image.unsplash.people(),
|
||||||
|
categoryIds: ['cat11'],
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author: bobDerBaumeister,
|
||||||
|
id: 'p13',
|
||||||
|
categoryIds: ['cat13'],
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author: jennyRostock,
|
||||||
|
id: 'p14',
|
||||||
|
image: faker.image.unsplash.objects(),
|
||||||
|
categoryIds: ['cat14'],
|
||||||
|
}),
|
||||||
|
factory.create('Post', {
|
||||||
|
author: huey,
|
||||||
|
id: 'p15',
|
||||||
|
categoryIds: ['cat15'],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
authenticatedUser = await louie.toJson()
|
||||||
const mention1 =
|
const mention1 =
|
||||||
'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
|
'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
|
||||||
const mention2 =
|
const mention2 =
|
||||||
@ -270,469 +415,272 @@ import Factory from './factories'
|
|||||||
const hashtag1 =
|
const hashtag1 =
|
||||||
'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!'
|
'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!'
|
||||||
const hashtagAndMention1 =
|
const hashtagAndMention1 =
|
||||||
'The new physics of <a class="hashtag" href="/search/hashtag/QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" href="/search/hashtag/QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u3" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
|
'The new physics of <a class="hashtag" href="/search/hashtag/QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" href="/search/hashtag/QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
|
||||||
|
const createPostMutation = gql`
|
||||||
|
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||||
|
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asAdmin.create('Post', {
|
mutate({
|
||||||
id: 'p0',
|
mutation: createPostMutation,
|
||||||
image: faker.image.unsplash.food(),
|
variables: {
|
||||||
categoryIds: ['cat16'],
|
id: 'p2',
|
||||||
|
title: `Nature Philosophy Yoga`,
|
||||||
|
content: hashtag1,
|
||||||
|
categoryIds: ['cat2'],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
asModerator.create('Post', {
|
mutate({
|
||||||
id: 'p1',
|
mutation: createPostMutation,
|
||||||
image: faker.image.unsplash.technology(),
|
variables: {
|
||||||
categoryIds: ['cat1'],
|
id: 'p7',
|
||||||
|
title: 'This is post #7',
|
||||||
|
content: `${mention1} ${faker.lorem.paragraph()}`,
|
||||||
|
categoryIds: ['cat7'],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
asUser.create('Post', {
|
mutate({
|
||||||
id: 'p2',
|
mutation: createPostMutation,
|
||||||
title: `Nature Philosophy Yoga`,
|
variables: {
|
||||||
content: `${hashtag1}`,
|
id: 'p8',
|
||||||
categoryIds: ['cat2'],
|
image: faker.image.unsplash.nature(),
|
||||||
|
title: `Quantum Flow Theory explains Quantum Gravity`,
|
||||||
|
content: hashtagAndMention1,
|
||||||
|
categoryIds: ['cat8'],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
asTick.create('Post', {
|
mutate({
|
||||||
id: 'p3',
|
mutation: createPostMutation,
|
||||||
categoryIds: ['cat3'],
|
variables: {
|
||||||
}),
|
id: 'p12',
|
||||||
asTrick.create('Post', {
|
title: 'This is post #12',
|
||||||
id: 'p4',
|
content: `${mention2} ${faker.lorem.paragraph()}`,
|
||||||
categoryIds: ['cat4'],
|
categoryIds: ['cat12'],
|
||||||
}),
|
},
|
||||||
asTrack.create('Post', {
|
|
||||||
id: 'p5',
|
|
||||||
categoryIds: ['cat5'],
|
|
||||||
}),
|
|
||||||
asAdmin.create('Post', {
|
|
||||||
id: 'p6',
|
|
||||||
image: faker.image.unsplash.buildings(),
|
|
||||||
categoryIds: ['cat6'],
|
|
||||||
}),
|
|
||||||
asModerator.create('Post', {
|
|
||||||
id: 'p7',
|
|
||||||
content: `${mention1} ${faker.lorem.paragraph()}`,
|
|
||||||
categoryIds: ['cat7'],
|
|
||||||
}),
|
|
||||||
asUser.create('Post', {
|
|
||||||
id: 'p8',
|
|
||||||
image: faker.image.unsplash.nature(),
|
|
||||||
title: `Quantum Flow Theory explains Quantum Gravity`,
|
|
||||||
content: `${hashtagAndMention1}`,
|
|
||||||
categoryIds: ['cat8'],
|
|
||||||
}),
|
|
||||||
asTick.create('Post', {
|
|
||||||
id: 'p9',
|
|
||||||
categoryIds: ['cat9'],
|
|
||||||
}),
|
|
||||||
asTrick.create('Post', {
|
|
||||||
id: 'p10',
|
|
||||||
categoryIds: ['cat10'],
|
|
||||||
}),
|
|
||||||
asTrack.create('Post', {
|
|
||||||
id: 'p11',
|
|
||||||
image: faker.image.unsplash.people(),
|
|
||||||
categoryIds: ['cat11'],
|
|
||||||
}),
|
|
||||||
asAdmin.create('Post', {
|
|
||||||
id: 'p12',
|
|
||||||
content: `${mention2} ${faker.lorem.paragraph()}`,
|
|
||||||
categoryIds: ['cat12'],
|
|
||||||
}),
|
|
||||||
asModerator.create('Post', {
|
|
||||||
id: 'p13',
|
|
||||||
categoryIds: ['cat13'],
|
|
||||||
}),
|
|
||||||
asUser.create('Post', {
|
|
||||||
id: 'p14',
|
|
||||||
image: faker.image.unsplash.objects(),
|
|
||||||
categoryIds: ['cat14'],
|
|
||||||
}),
|
|
||||||
asTick.create('Post', {
|
|
||||||
id: 'p15',
|
|
||||||
categoryIds: ['cat15'],
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
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',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u1',
|
|
||||||
to: 'p15',
|
|
||||||
data: 'surprised',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u2',
|
|
||||||
to: 'p15',
|
|
||||||
data: 'surprised',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u3',
|
|
||||||
to: 'p15',
|
|
||||||
data: 'surprised',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u4',
|
|
||||||
to: 'p15',
|
|
||||||
data: 'surprised',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u5',
|
|
||||||
to: 'p15',
|
|
||||||
data: 'surprised',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u6',
|
|
||||||
to: 'p15',
|
|
||||||
data: 'surprised',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u7',
|
|
||||||
to: 'p15',
|
|
||||||
data: 'surprised',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u2',
|
|
||||||
to: 'p14',
|
|
||||||
data: 'cry',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u3',
|
|
||||||
to: 'p13',
|
|
||||||
data: 'angry',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u4',
|
|
||||||
to: 'p12',
|
|
||||||
data: 'funny',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u5',
|
|
||||||
to: 'p11',
|
|
||||||
data: 'surprised',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u6',
|
|
||||||
to: 'p10',
|
|
||||||
data: 'cry',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u5',
|
|
||||||
to: 'p9',
|
|
||||||
data: 'happy',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u4',
|
|
||||||
to: 'p8',
|
|
||||||
data: 'angry',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u3',
|
|
||||||
to: 'p7',
|
|
||||||
data: 'funny',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u2',
|
|
||||||
to: 'p6',
|
|
||||||
data: 'surprised',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u1',
|
|
||||||
to: 'p5',
|
|
||||||
data: 'cry',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u2',
|
|
||||||
to: 'p4',
|
|
||||||
data: 'happy',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u3',
|
|
||||||
to: 'p3',
|
|
||||||
data: 'angry',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u4',
|
|
||||||
to: 'p2',
|
|
||||||
data: 'funny',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u5',
|
|
||||||
to: 'p1',
|
|
||||||
data: 'surprised',
|
|
||||||
}),
|
|
||||||
f.emote({
|
|
||||||
from: 'u6',
|
|
||||||
to: 'p0',
|
|
||||||
data: 'cry',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
asAdmin.shout({
|
|
||||||
id: 'p2',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asAdmin.shout({
|
|
||||||
id: 'p6',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asModerator.shout({
|
|
||||||
id: 'p0',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asModerator.shout({
|
|
||||||
id: 'p6',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asUser.shout({
|
|
||||||
id: 'p6',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asUser.shout({
|
|
||||||
id: 'p7',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asTick.shout({
|
|
||||||
id: 'p8',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asTick.shout({
|
|
||||||
id: 'p9',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asTrack.shout({
|
|
||||||
id: 'p10',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
await Promise.all([
|
|
||||||
asAdmin.shout({
|
|
||||||
id: 'p2',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asAdmin.shout({
|
|
||||||
id: 'p6',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asModerator.shout({
|
|
||||||
id: 'p0',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asModerator.shout({
|
|
||||||
id: 'p6',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asUser.shout({
|
|
||||||
id: 'p6',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asUser.shout({
|
|
||||||
id: 'p7',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asTick.shout({
|
|
||||||
id: 'p8',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asTick.shout({
|
|
||||||
id: 'p9',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
|
||||||
asTrack.shout({
|
|
||||||
id: 'p10',
|
|
||||||
type: 'Post',
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
const [p2, p7, p8, p12] = await Promise.all(
|
||||||
|
['p2', 'p7', 'p8', 'p12'].map(id => neode.find('Post', id)),
|
||||||
|
)
|
||||||
|
authenticatedUser = null
|
||||||
|
|
||||||
|
authenticatedUser = await dewey.toJson()
|
||||||
const mentionInComment1 =
|
const mentionInComment1 =
|
||||||
'I heard <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, practice it since 3 years now.'
|
'I heard <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, practice it since 3 years now.'
|
||||||
const mentionInComment2 =
|
const mentionInComment2 =
|
||||||
'Did <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> told you?'
|
'Did <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> told you?'
|
||||||
|
const createCommentMutation = gql`
|
||||||
|
mutation($id: ID, $postId: ID!, $content: String!) {
|
||||||
|
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
await Promise.all([
|
||||||
|
mutate({
|
||||||
|
mutation: createCommentMutation,
|
||||||
|
variables: {
|
||||||
|
id: 'c4',
|
||||||
|
postId: 'p2',
|
||||||
|
content: mentionInComment1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
mutate({
|
||||||
|
mutation: createCommentMutation,
|
||||||
|
variables: {
|
||||||
|
id: 'c4-1',
|
||||||
|
postId: 'p2',
|
||||||
|
content: mentionInComment2,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
mutate({
|
||||||
|
mutation: createCommentMutation,
|
||||||
|
variables: {
|
||||||
|
postId: 'p14',
|
||||||
|
content: faker.lorem.paragraph(),
|
||||||
|
},
|
||||||
|
}), // should send a notification
|
||||||
|
])
|
||||||
|
authenticatedUser = null
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asUser.create('Comment', {
|
factory.create('Comment', {
|
||||||
|
author: jennyRostock,
|
||||||
id: 'c1',
|
id: 'c1',
|
||||||
postId: 'p1',
|
postId: 'p1',
|
||||||
}),
|
}),
|
||||||
asTick.create('Comment', {
|
factory.create('Comment', {
|
||||||
|
author: huey,
|
||||||
id: 'c2',
|
id: 'c2',
|
||||||
postId: 'p1',
|
postId: 'p1',
|
||||||
}),
|
}),
|
||||||
asTrack.create('Comment', {
|
factory.create('Comment', {
|
||||||
|
author: louie,
|
||||||
id: 'c3',
|
id: 'c3',
|
||||||
postId: 'p3',
|
postId: 'p3',
|
||||||
}),
|
}),
|
||||||
asTrick.create('Comment', {
|
factory.create('Comment', {
|
||||||
id: 'c4',
|
author: bobDerBaumeister,
|
||||||
postId: 'p2',
|
|
||||||
content: `${mentionInComment1}`,
|
|
||||||
}),
|
|
||||||
asUser.create('Comment', {
|
|
||||||
id: 'c4-1',
|
|
||||||
postId: 'p2',
|
|
||||||
content: `${mentionInComment2}`,
|
|
||||||
}),
|
|
||||||
asModerator.create('Comment', {
|
|
||||||
id: 'c5',
|
id: 'c5',
|
||||||
postId: 'p3',
|
postId: 'p3',
|
||||||
}),
|
}),
|
||||||
asAdmin.create('Comment', {
|
factory.create('Comment', {
|
||||||
|
author: peterLustig,
|
||||||
id: 'c6',
|
id: 'c6',
|
||||||
postId: 'p4',
|
postId: 'p4',
|
||||||
}),
|
}),
|
||||||
asUser.create('Comment', {
|
factory.create('Comment', {
|
||||||
|
author: jennyRostock,
|
||||||
id: 'c7',
|
id: 'c7',
|
||||||
postId: 'p2',
|
postId: 'p2',
|
||||||
}),
|
}),
|
||||||
asTick.create('Comment', {
|
factory.create('Comment', {
|
||||||
|
author: huey,
|
||||||
id: 'c8',
|
id: 'c8',
|
||||||
postId: 'p15',
|
postId: 'p15',
|
||||||
}),
|
}),
|
||||||
asTrick.create('Comment', {
|
factory.create('Comment', {
|
||||||
|
author: dewey,
|
||||||
id: 'c9',
|
id: 'c9',
|
||||||
postId: 'p15',
|
postId: 'p15',
|
||||||
}),
|
}),
|
||||||
asTrack.create('Comment', {
|
factory.create('Comment', {
|
||||||
|
author: louie,
|
||||||
id: 'c10',
|
id: 'c10',
|
||||||
postId: 'p15',
|
postId: 'p15',
|
||||||
}),
|
}),
|
||||||
asUser.create('Comment', {
|
factory.create('Comment', {
|
||||||
|
author: jennyRostock,
|
||||||
id: 'c11',
|
id: 'c11',
|
||||||
postId: 'p15',
|
postId: 'p15',
|
||||||
}),
|
}),
|
||||||
asUser.create('Comment', {
|
factory.create('Comment', {
|
||||||
|
author: jennyRostock,
|
||||||
id: 'c12',
|
id: 'c12',
|
||||||
postId: 'p15',
|
postId: 'p15',
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const disableMutation = 'mutation($id: ID!) { disable(id: $id) }'
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asModerator.mutate(disableMutation, {
|
democracy.relateTo(p3, 'post'),
|
||||||
id: 'p11',
|
democracy.relateTo(p11, 'post'),
|
||||||
}),
|
democracy.relateTo(p15, 'post'),
|
||||||
asModerator.mutate(disableMutation, {
|
democracy.relateTo(p7, 'post'),
|
||||||
id: 'c5',
|
environment.relateTo(p1, 'post'),
|
||||||
}),
|
environment.relateTo(p5, 'post'),
|
||||||
|
environment.relateTo(p9, 'post'),
|
||||||
|
environment.relateTo(p13, 'post'),
|
||||||
|
freedom.relateTo(p0, 'post'),
|
||||||
|
freedom.relateTo(p4, 'post'),
|
||||||
|
freedom.relateTo(p8, 'post'),
|
||||||
|
freedom.relateTo(p12, 'post'),
|
||||||
|
nature.relateTo(p2, 'post'),
|
||||||
|
nature.relateTo(p6, 'post'),
|
||||||
|
nature.relateTo(p10, 'post'),
|
||||||
|
nature.relateTo(p14, 'post'),
|
||||||
|
peterLustig.relateTo(p15, 'emoted', { emotion: 'surprised' }),
|
||||||
|
bobDerBaumeister.relateTo(p15, 'emoted', { emotion: 'surprised' }),
|
||||||
|
jennyRostock.relateTo(p15, 'emoted', { emotion: 'surprised' }),
|
||||||
|
huey.relateTo(p15, 'emoted', { emotion: 'surprised' }),
|
||||||
|
dewey.relateTo(p15, 'emoted', { emotion: 'surprised' }),
|
||||||
|
louie.relateTo(p15, 'emoted', { emotion: 'surprised' }),
|
||||||
|
dagobert.relateTo(p15, 'emoted', { emotion: 'surprised' }),
|
||||||
|
bobDerBaumeister.relateTo(p14, 'emoted', { emotion: 'cry' }),
|
||||||
|
jennyRostock.relateTo(p13, 'emoted', { emotion: 'angry' }),
|
||||||
|
huey.relateTo(p12, 'emoted', { emotion: 'funny' }),
|
||||||
|
dewey.relateTo(p11, 'emoted', { emotion: 'surprised' }),
|
||||||
|
louie.relateTo(p10, 'emoted', { emotion: 'cry' }),
|
||||||
|
dewey.relateTo(p9, 'emoted', { emotion: 'happy' }),
|
||||||
|
huey.relateTo(p8, 'emoted', { emotion: 'angry' }),
|
||||||
|
jennyRostock.relateTo(p7, 'emoted', { emotion: 'funny' }),
|
||||||
|
bobDerBaumeister.relateTo(p6, 'emoted', { emotion: 'surprised' }),
|
||||||
|
peterLustig.relateTo(p5, 'emoted', { emotion: 'cry' }),
|
||||||
|
bobDerBaumeister.relateTo(p4, 'emoted', { emotion: 'happy' }),
|
||||||
|
jennyRostock.relateTo(p3, 'emoted', { emotion: 'angry' }),
|
||||||
|
huey.relateTo(p2, 'emoted', { emotion: 'funny' }),
|
||||||
|
dewey.relateTo(p1, 'emoted', { emotion: 'surprised' }),
|
||||||
|
louie.relateTo(p0, 'emoted', { emotion: 'cry' }),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asTick.create('Report', {
|
peterLustig.relateTo(p1, 'shouted'),
|
||||||
description: "I don't like this comment",
|
peterLustig.relateTo(p6, 'shouted'),
|
||||||
id: 'c1',
|
bobDerBaumeister.relateTo(p0, 'shouted'),
|
||||||
}),
|
bobDerBaumeister.relateTo(p6, 'shouted'),
|
||||||
asTrick.create('Report', {
|
jennyRostock.relateTo(p6, 'shouted'),
|
||||||
description: "I don't like this post",
|
jennyRostock.relateTo(p7, 'shouted'),
|
||||||
id: 'p1',
|
huey.relateTo(p8, 'shouted'),
|
||||||
}),
|
huey.relateTo(p9, 'shouted'),
|
||||||
asTrack.create('Report', {
|
dewey.relateTo(p10, 'shouted'),
|
||||||
description: "I don't like this user",
|
peterLustig.relateTo(p2, 'shouted'),
|
||||||
id: 'u1',
|
peterLustig.relateTo(p6, 'shouted'),
|
||||||
}),
|
bobDerBaumeister.relateTo(p0, 'shouted'),
|
||||||
|
bobDerBaumeister.relateTo(p6, 'shouted'),
|
||||||
|
jennyRostock.relateTo(p6, 'shouted'),
|
||||||
|
jennyRostock.relateTo(p7, 'shouted'),
|
||||||
|
huey.relateTo(p8, 'shouted'),
|
||||||
|
huey.relateTo(p9, 'shouted'),
|
||||||
|
louie.relateTo(p10, 'shouted'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const disableMutation = gql`
|
||||||
|
mutation($id: ID!) {
|
||||||
|
disable(id: $id)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
authenticatedUser = await bobDerBaumeister.toJson()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
f.create('Organization', {
|
mutate({
|
||||||
id: 'o1',
|
mutation: disableMutation,
|
||||||
name: 'Democracy Deutschland',
|
variables: {
|
||||||
description: 'Description for democracy-deutschland.',
|
id: 'p11',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
f.create('Organization', {
|
mutate({
|
||||||
id: 'o2',
|
mutation: disableMutation,
|
||||||
name: 'Human-Connection',
|
variables: {
|
||||||
description: 'Description for human-connection.',
|
id: 'c5',
|
||||||
}),
|
},
|
||||||
f.create('Organization', {
|
|
||||||
id: 'o3',
|
|
||||||
name: 'Pro Veg',
|
|
||||||
description: 'Description for pro-veg.',
|
|
||||||
}),
|
|
||||||
f.create('Organization', {
|
|
||||||
id: 'o4',
|
|
||||||
name: 'Greenpeace',
|
|
||||||
description: 'Description for greenpeace.',
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
authenticatedUser = null
|
||||||
|
|
||||||
|
const reportMutation = gql`
|
||||||
|
mutation($id: ID!, $description: String!) {
|
||||||
|
report(description: $description, id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
authenticatedUser = await huey.toJson()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
f.relate('Organization', 'CreatedBy', {
|
mutate({
|
||||||
from: 'u1',
|
mutation: reportMutation,
|
||||||
to: 'o1',
|
variables: {
|
||||||
|
description: "I don't like this comment",
|
||||||
|
id: 'c1',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
f.relate('Organization', 'CreatedBy', {
|
mutate({
|
||||||
from: 'u1',
|
mutation: reportMutation,
|
||||||
to: 'o2',
|
variables: {
|
||||||
|
description: "I don't like this post",
|
||||||
|
id: 'p1',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
f.relate('Organization', 'OwnedBy', {
|
mutate({
|
||||||
from: 'u2',
|
mutation: reportMutation,
|
||||||
to: 'o2',
|
variables: {
|
||||||
}),
|
description: "I don't like this user",
|
||||||
f.relate('Organization', 'OwnedBy', {
|
id: 'u1',
|
||||||
from: 'u2',
|
},
|
||||||
to: 'o3',
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
authenticatedUser = null
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[...Array(30).keys()].map(i => {
|
[...Array(30).keys()].map(i => {
|
||||||
|
|||||||
@ -18,20 +18,22 @@ Object.entries(requiredConfigs).map(entry => {
|
|||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
const neode = getNeode()
|
const neode = getNeode()
|
||||||
|
|
||||||
|
export const context = async ({ req }) => {
|
||||||
|
const user = await decode(driver, req.headers.authorization)
|
||||||
|
return {
|
||||||
|
driver,
|
||||||
|
neode,
|
||||||
|
user,
|
||||||
|
req,
|
||||||
|
cypherParams: {
|
||||||
|
currentUserId: user ? user.id : null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createServer = options => {
|
const createServer = options => {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
context: async ({ req }) => {
|
context,
|
||||||
const user = await decode(driver, req.headers.authorization)
|
|
||||||
return {
|
|
||||||
driver,
|
|
||||||
neode,
|
|
||||||
user,
|
|
||||||
req,
|
|
||||||
cypherParams: {
|
|
||||||
currentUserId: user ? user.id : null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
schema: middleware(schema),
|
schema: middleware(schema),
|
||||||
debug: !!CONFIG.DEBUG,
|
debug: !!CONFIG.DEBUG,
|
||||||
tracing: !!CONFIG.DEBUG,
|
tracing: !!CONFIG.DEBUG,
|
||||||
|
|||||||
@ -689,7 +689,7 @@
|
|||||||
core-js "^2.6.5"
|
core-js "^2.6.5"
|
||||||
regenerator-runtime "^0.13.2"
|
regenerator-runtime "^0.13.2"
|
||||||
|
|
||||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5":
|
"@babel/runtime@^7.0.0", "@babel/runtime@^7.5.5":
|
||||||
version "7.5.5"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
|
||||||
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
|
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
|
||||||
@ -1538,13 +1538,13 @@ anymatch@^2.0.0:
|
|||||||
micromatch "^3.1.4"
|
micromatch "^3.1.4"
|
||||||
normalize-path "^2.1.1"
|
normalize-path "^2.1.1"
|
||||||
|
|
||||||
apollo-cache-control@0.8.2:
|
apollo-cache-control@^0.8.4:
|
||||||
version "0.8.2"
|
version "0.8.4"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.2.tgz#0687e323053f907fd9bb601c1921de10799e24a0"
|
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.4.tgz#a3650d5e4173953e2a3af995bea62147f1ffe4d7"
|
||||||
integrity sha512-rvx4DdoAAbWhm3L0IoWrxN+Zq2Xk4uAYbaiZk0Nhuc/y4AQUww3JV/z4EfCp3O5cy5/lNMW/tPOozcqi941awA==
|
integrity sha512-IZ1d3AXZtkZhLYo0kWqTbZ6nqLFaeUvLdMESs+9orMadBZ7mvzcAfBwrhKyCWPGeAAZ/jKv8FtYHybpchHgFAg==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-env "2.4.2"
|
apollo-server-env "^2.4.3"
|
||||||
graphql-extensions "0.10.1"
|
graphql-extensions "^0.10.3"
|
||||||
|
|
||||||
apollo-cache-inmemory@~1.6.3:
|
apollo-cache-inmemory@~1.6.3:
|
||||||
version "1.6.3"
|
version "1.6.3"
|
||||||
@ -1579,33 +1579,33 @@ apollo-client@~2.6.4:
|
|||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
zen-observable "^0.8.0"
|
zen-observable "^0.8.0"
|
||||||
|
|
||||||
apollo-datasource@0.6.2:
|
apollo-datasource@^0.6.3:
|
||||||
version "0.6.2"
|
version "0.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.2.tgz#3eeb8f9660304a223c3f7aecfe0274376c876307"
|
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.3.tgz#b31e089e52adb92fabb536ab8501c502573ffe13"
|
||||||
integrity sha512-GlqTfLjKFxNYxGGACDjDXUpm/vPfvXhUI/Qc/YdkY4ess/wn7EFdrmbZGIY56RJtXD5M7qjsQIH15t132KoPmQ==
|
integrity sha512-gRYyFVpJgHE2hhS+VxMeOerxXQ/QYxWG7T6QddfugJWYAG9DRCl65e2b7txcGq2NP3r+O1iCm4GNwhRBDJbd8A==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-caching "0.5.0"
|
apollo-server-caching "^0.5.0"
|
||||||
apollo-server-env "2.4.2"
|
apollo-server-env "^2.4.3"
|
||||||
|
|
||||||
apollo-engine-reporting-protobuf@0.4.0:
|
apollo-engine-reporting-protobuf@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.0.tgz#e34c192d86493b33a73181fd6be75721559111ec"
|
resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.0.tgz#e34c192d86493b33a73181fd6be75721559111ec"
|
||||||
integrity sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA==
|
integrity sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA==
|
||||||
dependencies:
|
dependencies:
|
||||||
protobufjs "^6.8.6"
|
protobufjs "^6.8.6"
|
||||||
|
|
||||||
apollo-engine-reporting@1.4.4:
|
apollo-engine-reporting@^1.4.6:
|
||||||
version "1.4.4"
|
version "1.4.6"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.4.tgz#ab232dcaa81fe9718fb23e9782457c66dc86e817"
|
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.6.tgz#83af6689c4ab82d1c62c3f5dde7651975508114f"
|
||||||
integrity sha512-FOk/HooLMesoKHo/TGOPYZuc2t4q9YwoeM+z0AGRUY70hL2o5Ie3x0XiMb+I5IVibR+jBIRRKP2ngmSFJ+LqSg==
|
integrity sha512-acfb7oFnru/8YQdY4x6+7WJbZfzdVETI8Cl+9ImgUrvUnE8P+f2SsGTKXTC1RuUvve4c56PAvaPgE+z8X1a1Mw==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-engine-reporting-protobuf "0.4.0"
|
apollo-engine-reporting-protobuf "^0.4.0"
|
||||||
apollo-graphql "^0.3.3"
|
apollo-graphql "^0.3.3"
|
||||||
apollo-server-caching "0.5.0"
|
apollo-server-caching "^0.5.0"
|
||||||
apollo-server-env "2.4.2"
|
apollo-server-env "^2.4.3"
|
||||||
apollo-server-types "0.2.2"
|
apollo-server-types "^0.2.4"
|
||||||
async-retry "^1.2.1"
|
async-retry "^1.2.1"
|
||||||
graphql-extensions "0.10.1"
|
graphql-extensions "^0.10.3"
|
||||||
|
|
||||||
apollo-env@0.5.1:
|
apollo-env@0.5.1:
|
||||||
version "0.5.1"
|
version "0.5.1"
|
||||||
@ -1668,33 +1668,33 @@ apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.3:
|
|||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
zen-observable-ts "^0.8.19"
|
zen-observable-ts "^0.8.19"
|
||||||
|
|
||||||
apollo-server-caching@0.5.0:
|
apollo-server-caching@^0.5.0:
|
||||||
version "0.5.0"
|
version "0.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46"
|
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46"
|
||||||
integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw==
|
integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw==
|
||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^5.0.0"
|
lru-cache "^5.0.0"
|
||||||
|
|
||||||
apollo-server-core@2.9.1:
|
apollo-server-core@^2.9.3:
|
||||||
version "2.9.1"
|
version "2.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.1.tgz#ed876cd2f954dc3f4f1e735b997d4dbf29a629a5"
|
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.3.tgz#918f836c8215d371935c831c72d0840c7bf0250f"
|
||||||
integrity sha512-ZWPGNdZv/SiPjfEU7Wwut9N9oAucGlbVT+XCnpUl93agvkg3fbeTCLYBbjAdSA0Q6opq0tvWVGzwXPLpx6jZcQ==
|
integrity sha512-KQpOM3nAXdMqKVE0HHcOkH/EVhyDqFEKLNFlsyGHGOn9ujpI6RsltX+YpXRyAdbfQHpTk11v/IAo6XksWN+g1Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@apollographql/apollo-tools" "^0.4.0"
|
"@apollographql/apollo-tools" "^0.4.0"
|
||||||
"@apollographql/graphql-playground-html" "1.6.24"
|
"@apollographql/graphql-playground-html" "1.6.24"
|
||||||
"@types/graphql-upload" "^8.0.0"
|
"@types/graphql-upload" "^8.0.0"
|
||||||
"@types/ws" "^6.0.0"
|
"@types/ws" "^6.0.0"
|
||||||
apollo-cache-control "0.8.2"
|
apollo-cache-control "^0.8.4"
|
||||||
apollo-datasource "0.6.2"
|
apollo-datasource "^0.6.3"
|
||||||
apollo-engine-reporting "1.4.4"
|
apollo-engine-reporting "^1.4.6"
|
||||||
apollo-server-caching "0.5.0"
|
apollo-server-caching "^0.5.0"
|
||||||
apollo-server-env "2.4.2"
|
apollo-server-env "^2.4.3"
|
||||||
apollo-server-errors "2.3.2"
|
apollo-server-errors "^2.3.3"
|
||||||
apollo-server-plugin-base "0.6.2"
|
apollo-server-plugin-base "^0.6.4"
|
||||||
apollo-server-types "0.2.2"
|
apollo-server-types "^0.2.4"
|
||||||
apollo-tracing "0.8.2"
|
apollo-tracing "^0.8.4"
|
||||||
fast-json-stable-stringify "^2.0.0"
|
fast-json-stable-stringify "^2.0.0"
|
||||||
graphql-extensions "0.10.1"
|
graphql-extensions "^0.10.3"
|
||||||
graphql-tag "^2.9.2"
|
graphql-tag "^2.9.2"
|
||||||
graphql-tools "^4.0.0"
|
graphql-tools "^4.0.0"
|
||||||
graphql-upload "^8.0.2"
|
graphql-upload "^8.0.2"
|
||||||
@ -1702,23 +1702,23 @@ apollo-server-core@2.9.1:
|
|||||||
subscriptions-transport-ws "^0.9.11"
|
subscriptions-transport-ws "^0.9.11"
|
||||||
ws "^6.0.0"
|
ws "^6.0.0"
|
||||||
|
|
||||||
apollo-server-env@2.4.2:
|
apollo-server-env@^2.4.3:
|
||||||
version "2.4.2"
|
version "2.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.2.tgz#8549caa7c8f57af88aadad5c2a0bb7adbcc5f76e"
|
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.3.tgz#9bceedaae07eafb96becdfd478f8d92617d825d2"
|
||||||
integrity sha512-Qyi8fP8CWsBRAKs0fawMFauJj03I6N3ncWcGaVTuDppYluo4zjV6LqHfZ+YPWOx6apBihFNZap19RAhSnSwJLg==
|
integrity sha512-23R5Xo9OMYX0iyTu2/qT0EUb+AULCBriA9w8HDfMoChB8M+lFClqUkYtaTTHDfp6eoARLW8kDBhPOBavsvKAjA==
|
||||||
dependencies:
|
dependencies:
|
||||||
node-fetch "^2.1.2"
|
node-fetch "^2.1.2"
|
||||||
util.promisify "^1.0.0"
|
util.promisify "^1.0.0"
|
||||||
|
|
||||||
apollo-server-errors@2.3.2:
|
apollo-server-errors@^2.3.3:
|
||||||
version "2.3.2"
|
version "2.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.2.tgz#86bbd1ff8f0b5f16bfdcbb1760398928f9fce539"
|
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.3.tgz#83763b00352c10dc68fbb0d41744ade66de549ff"
|
||||||
integrity sha512-twVCP8tNHFzxOzU3jf84ppBFSvjvisZVWlgF82vwG+qEEUaAE5h5DVpeJbcI1vRW4VQPuFV+B+FIsnlweFKqtQ==
|
integrity sha512-MO4oJ129vuCcbqwr5ZwgxqGGiLz3hCyowz0bstUF7MR+vNGe4oe3DWajC9lv4CxrhcqUHQOeOPViOdIo1IxE3g==
|
||||||
|
|
||||||
apollo-server-express@2.9.1, apollo-server-express@^2.9.0:
|
apollo-server-express@^2.9.0, apollo-server-express@^2.9.3:
|
||||||
version "2.9.1"
|
version "2.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.1.tgz#9a8cb7fba579e68ddfa1953dfd066b751bca32f0"
|
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.3.tgz#67573404030c2676be49a7bf97d423b8462e295c"
|
||||||
integrity sha512-3mmuojt9s9Gyqdf8fbdKtbw23UFYrtVQtTNASgVW8zCabZqs2WjYnijMRf1aL4u9VSl+BFMOZUPMYaeBX+u38w==
|
integrity sha512-Hkfs+ce6GqaoSzDOJs8Pj7W3YUjH0BzGglo5HMsOXOnjPZ0pJE9v8fmK76rlkITLw7GjvIq5GKlafymC31FMBw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@apollographql/graphql-playground-html" "1.6.24"
|
"@apollographql/graphql-playground-html" "1.6.24"
|
||||||
"@types/accepts" "^1.3.5"
|
"@types/accepts" "^1.3.5"
|
||||||
@ -1726,57 +1726,58 @@ apollo-server-express@2.9.1, apollo-server-express@^2.9.0:
|
|||||||
"@types/cors" "^2.8.4"
|
"@types/cors" "^2.8.4"
|
||||||
"@types/express" "4.17.1"
|
"@types/express" "4.17.1"
|
||||||
accepts "^1.3.5"
|
accepts "^1.3.5"
|
||||||
apollo-server-core "2.9.1"
|
apollo-server-core "^2.9.3"
|
||||||
apollo-server-types "0.2.2"
|
apollo-server-types "^0.2.4"
|
||||||
body-parser "^1.18.3"
|
body-parser "^1.18.3"
|
||||||
cors "^2.8.4"
|
cors "^2.8.4"
|
||||||
|
express "^4.17.1"
|
||||||
graphql-subscriptions "^1.0.0"
|
graphql-subscriptions "^1.0.0"
|
||||||
graphql-tools "^4.0.0"
|
graphql-tools "^4.0.0"
|
||||||
parseurl "^1.3.2"
|
parseurl "^1.3.2"
|
||||||
subscriptions-transport-ws "^0.9.16"
|
subscriptions-transport-ws "^0.9.16"
|
||||||
type-is "^1.6.16"
|
type-is "^1.6.16"
|
||||||
|
|
||||||
apollo-server-plugin-base@0.6.2:
|
apollo-server-plugin-base@^0.6.4:
|
||||||
version "0.6.2"
|
version "0.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.2.tgz#807e734e130c6750db680a58cd0e572cc0794184"
|
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.4.tgz#63ea4fd0bbb6c4510bc8d0d2ad0a0684c8d0da8c"
|
||||||
integrity sha512-f7grbfoI5fPxGJDmrvG0ulWq8vFHwvJSUrcEChhiUCSMFZlpBil/1TSaxJRESiQqnoZ9s5WrVhzuwejxODGMYw==
|
integrity sha512-4rY+cBAIpQomGWYBtk8hHkLQWHrh5hgIBPQqmhXh00YFdcY+Ob1/cU2/2iqTcIzhtcaezsc8OZ63au6ahSBQqg==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-types "0.2.2"
|
apollo-server-types "^0.2.4"
|
||||||
|
|
||||||
apollo-server-testing@~2.9.1:
|
apollo-server-testing@~2.9.3:
|
||||||
version "2.9.1"
|
version "2.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.1.tgz#29d2524e84722a1319d9c1524b4f9d44379d6a49"
|
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.3.tgz#38a86b5fa0bce57f8ec4fb581e5419437178b3e2"
|
||||||
integrity sha512-TzlHIYNZgF1OkGji/ew3zPxboifvA9aGXDwWJFu54o1400svH0Uh5L7TMhsTZ8F992syQUsUuI+KKMOFNg73+w==
|
integrity sha512-n2bIcVXQNFzr84FZK1S0o4PFqwb1pPuIg/fymjPYjtFP2OHmLLvGRm+KaXhUjxEAUh+/9zAQLhmgx+p6GMUAhA==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-core "2.9.1"
|
apollo-server-core "^2.9.3"
|
||||||
|
|
||||||
apollo-server-types@0.2.2:
|
apollo-server-types@^0.2.4:
|
||||||
version "0.2.2"
|
version "0.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.2.tgz#c26ff57ca0b45d67dfd72312094097e2b1c28980"
|
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.4.tgz#28864900ffc7f9711a859297c143a833fdb6aa43"
|
||||||
integrity sha512-/G4yXUF4Kc6PVCIF12r+oB8AXkE4UVnJoyZHeHiPeDpXklrjwIAtov2WM2mTcSZuZe1EuEkeDci4+tj5zFD39Q==
|
integrity sha512-G4FvBVgGQcTW6ZBS2+hvcDQkSfdOIKV+cHADduXA275v+5zl42g+bCaGd/hCCKTDRjmQvObLiMxH/BJ6pDMQgA==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-engine-reporting-protobuf "0.4.0"
|
apollo-engine-reporting-protobuf "^0.4.0"
|
||||||
apollo-server-caching "0.5.0"
|
apollo-server-caching "^0.5.0"
|
||||||
apollo-server-env "2.4.2"
|
apollo-server-env "^2.4.3"
|
||||||
|
|
||||||
apollo-server@~2.9.1:
|
apollo-server@~2.9.3:
|
||||||
version "2.9.1"
|
version "2.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.1.tgz#16ff443d43ea38f72fe20adea0803c46037b2b3b"
|
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.3.tgz#2a79fcee25da0b0673eb70d73839c40c3c4b8cca"
|
||||||
integrity sha512-iCGoRBOvwTUkDz6Nq/rKguMyhDiQdL3VneF0GTjBGrelTIp3YTIxk/qBFkIr2Chtm9ZZYkS6o+ZldUnxYFKg7A==
|
integrity sha512-JQoeseSo3yOBu3WJzju0NTreoqYckNILybgXNUOhdurE55VFpZ8dsBEO6nMfdO2y1A70W14mnnVWCBEm+1rE8w==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-core "2.9.1"
|
apollo-server-core "^2.9.3"
|
||||||
apollo-server-express "2.9.1"
|
apollo-server-express "^2.9.3"
|
||||||
express "^4.0.0"
|
express "^4.0.0"
|
||||||
graphql-subscriptions "^1.0.0"
|
graphql-subscriptions "^1.0.0"
|
||||||
graphql-tools "^4.0.0"
|
graphql-tools "^4.0.0"
|
||||||
|
|
||||||
apollo-tracing@0.8.2:
|
apollo-tracing@^0.8.4:
|
||||||
version "0.8.2"
|
version "0.8.4"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.2.tgz#2d1ebef434c4e2803f9a3adfc7d2409690b3c378"
|
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.4.tgz#0117820c3f0ad3aa6daf7bf13ddbb923cbefa6de"
|
||||||
integrity sha512-4SVxHZkKZX/7E6/4hAvEJXdHm+1BjQqtgEkv3ywyiVXoaKn0YNJL8BVIOI4GAt0qoc3KzT9MDJ1nf+SurUFjLQ==
|
integrity sha512-DjbFW0IvHicSlTVG+vK+1WINfBMRCdPPHJSW/j65JMir9Oe56WGeqL8qz8hptdUUmLYEb+azvcyyGsJsiR3zpQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-env "2.4.2"
|
apollo-server-env "^2.4.3"
|
||||||
graphql-extensions "0.10.1"
|
graphql-extensions "^0.10.3"
|
||||||
|
|
||||||
apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
|
apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
@ -2687,13 +2688,12 @@ create-error-class@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
capture-stack-trace "^1.0.0"
|
capture-stack-trace "^1.0.0"
|
||||||
|
|
||||||
cross-env@~5.2.0:
|
cross-env@~5.2.1:
|
||||||
version "5.2.0"
|
version "5.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2"
|
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.1.tgz#b2c76c1ca7add66dc874d11798466094f551b34d"
|
||||||
integrity sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==
|
integrity sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn "^6.0.5"
|
cross-spawn "^6.0.5"
|
||||||
is-windows "^1.0.0"
|
|
||||||
|
|
||||||
cross-fetch@2.2.2:
|
cross-fetch@2.2.2:
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
@ -3311,12 +3311,12 @@ eslint-module-utils@^2.4.0:
|
|||||||
debug "^2.6.8"
|
debug "^2.6.8"
|
||||||
pkg-dir "^2.0.0"
|
pkg-dir "^2.0.0"
|
||||||
|
|
||||||
eslint-plugin-es@^1.4.0:
|
eslint-plugin-es@^1.4.1:
|
||||||
version "1.4.0"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz#12acae0f4953e76ba444bfd1b2271081ac620998"
|
||||||
integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw==
|
integrity sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint-utils "^1.3.0"
|
eslint-utils "^1.4.2"
|
||||||
regexpp "^2.0.1"
|
regexpp "^2.0.1"
|
||||||
|
|
||||||
eslint-plugin-import@~2.18.2:
|
eslint-plugin-import@~2.18.2:
|
||||||
@ -3336,20 +3336,20 @@ eslint-plugin-import@~2.18.2:
|
|||||||
read-pkg-up "^2.0.0"
|
read-pkg-up "^2.0.0"
|
||||||
resolve "^1.11.0"
|
resolve "^1.11.0"
|
||||||
|
|
||||||
eslint-plugin-jest@~22.15.2:
|
eslint-plugin-jest@~22.16.0:
|
||||||
version "22.15.2"
|
version "22.16.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.2.tgz#e3c10d9391f787744e31566f69ebb70c3a98e398"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.16.0.tgz#30c4e0e9dc331beb2e7369b70dd1363690c1ce05"
|
||||||
integrity sha512-p4NME9TgXIt+KgpxcXyNBvO30ZKxwFAO1dJZBc2OGfDnXVEtPwEyNs95GSr6RIE3xLHdjd8ngDdE2icRRXrbxg==
|
integrity sha512-eBtSCDhO1k7g3sULX/fuRK+upFQ7s548rrBtxDyM1fSoY7dTWp/wICjrJcDZKVsW7tsFfH22SG+ZaxG5BZodIg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/experimental-utils" "^1.13.0"
|
"@typescript-eslint/experimental-utils" "^1.13.0"
|
||||||
|
|
||||||
eslint-plugin-node@~9.1.0:
|
eslint-plugin-node@~9.2.0:
|
||||||
version "9.1.0"
|
version "9.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.2.0.tgz#b1911f111002d366c5954a6d96d3cd5bf2a3036a"
|
||||||
integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw==
|
integrity sha512-2abNmzAH/JpxI4gEOwd6K8wZIodK3BmHbTxz4s79OIYwwIt2gkpEXlAouJXu4H1c9ySTnRso0tsuthSOZbUMlA==
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint-plugin-es "^1.4.0"
|
eslint-plugin-es "^1.4.1"
|
||||||
eslint-utils "^1.3.1"
|
eslint-utils "^1.4.2"
|
||||||
ignore "^5.1.1"
|
ignore "^5.1.1"
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
resolve "^1.10.1"
|
resolve "^1.10.1"
|
||||||
@ -3388,7 +3388,7 @@ eslint-scope@^5.0.0:
|
|||||||
esrecurse "^4.1.0"
|
esrecurse "^4.1.0"
|
||||||
estraverse "^4.1.1"
|
estraverse "^4.1.1"
|
||||||
|
|
||||||
eslint-utils@^1.3.0, eslint-utils@^1.3.1, eslint-utils@^1.4.2:
|
eslint-utils@^1.4.2:
|
||||||
version "1.4.2"
|
version "1.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
|
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
|
||||||
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
|
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
|
||||||
@ -3400,10 +3400,10 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
|
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
|
||||||
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
|
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
|
||||||
|
|
||||||
eslint@~6.2.2:
|
eslint@~6.3.0:
|
||||||
version "6.2.2"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.2.2.tgz#03298280e7750d81fcd31431f3d333e43d93f24f"
|
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.3.0.tgz#1f1a902f67bfd4c354e7288b81e40654d927eb6a"
|
||||||
integrity sha512-mf0elOkxHbdyGX1IJEUsNBzCDdyoUgljF3rRlgfyYh0pwGnreLc0jjD6ZuleOibjmnUWZLY2eXwSooeOgGJ2jw==
|
integrity sha512-ZvZTKaqDue+N8Y9g0kp6UPZtS4FSY3qARxBs7p4f0H0iof381XHduqVerFWtK8DPtKmemqbqCFENWSQgPR/Gow==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.0.0"
|
"@babel/code-frame" "^7.0.0"
|
||||||
ajv "^6.10.0"
|
ajv "^6.10.0"
|
||||||
@ -4077,14 +4077,14 @@ graphql-custom-directives@~0.2.14:
|
|||||||
moment "^2.22.2"
|
moment "^2.22.2"
|
||||||
numeral "^2.0.6"
|
numeral "^2.0.6"
|
||||||
|
|
||||||
graphql-extensions@0.10.1:
|
graphql-extensions@^0.10.3:
|
||||||
version "0.10.1"
|
version "0.10.3"
|
||||||
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.10.1.tgz#9e1abd502f3f802a7ab60c3a28d2fe705e53d4cb"
|
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.10.3.tgz#9e37f3bd26309c40b03a0be0e63e02b3f99d52ea"
|
||||||
integrity sha512-RIlC/jgBKZ/qyrb+cAu7oJVYLC0dJh6al35tNy8dnqE9JImNucy/gFWVOPW7q3fAaXqCHzbBEtdb+ws1L43LgQ==
|
integrity sha512-kwU0gUe+Qdfr8iZYT91qrPSwQNgPhB/ClF1m1LEPdxlptk5FhFmjpxAcbMZ8q7j0kjfnbp2IeV1OhRDCEPqz2w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@apollographql/apollo-tools" "^0.4.0"
|
"@apollographql/apollo-tools" "^0.4.0"
|
||||||
apollo-server-env "2.4.2"
|
apollo-server-env "^2.4.3"
|
||||||
apollo-server-types "0.2.2"
|
apollo-server-types "^0.2.4"
|
||||||
|
|
||||||
graphql-import@0.7.1:
|
graphql-import@0.7.1:
|
||||||
version "0.7.1"
|
version "0.7.1"
|
||||||
@ -4180,10 +4180,10 @@ graphql-upload@^8.0.2:
|
|||||||
http-errors "^1.7.2"
|
http-errors "^1.7.2"
|
||||||
object-path "^0.11.4"
|
object-path "^0.11.4"
|
||||||
|
|
||||||
graphql@^14.2.1, graphql@^14.5.3:
|
graphql@^14.2.1, graphql@^14.5.4:
|
||||||
version "14.5.3"
|
version "14.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.3.tgz#e025851cc413e153220f4edbbb25d49f55104fa0"
|
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.4.tgz#b33fe957854e90c10d4c07c7d26b6c8e9f159a13"
|
||||||
integrity sha512-W8A8nt9BsMg0ZK2qA3DJIVU6muWhxZRYLTmc+5XGwzWzVdUdPVlAAg5hTBjiTISEnzsKL/onasu6vl3kgGTbYg==
|
integrity sha512-dPLvHoxy5m9FrkqWczPPRnH0X80CyvRE6e7Fa5AWEqEAzg9LpxHvKh24po/482E6VWHigOkAmb4xCp6P9yT9gw==
|
||||||
dependencies:
|
dependencies:
|
||||||
iterall "^1.2.2"
|
iterall "^1.2.2"
|
||||||
|
|
||||||
@ -4875,7 +4875,7 @@ is-valid-path@0.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-invalid-path "^0.1.0"
|
is-invalid-path "^0.1.0"
|
||||||
|
|
||||||
is-windows@^1.0.0, is-windows@^1.0.2:
|
is-windows@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||||
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
|
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
|
||||||
@ -6175,12 +6175,12 @@ neo-async@^2.6.0:
|
|||||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
||||||
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
|
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
|
||||||
|
|
||||||
neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.5:
|
neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.6:
|
||||||
version "1.7.5"
|
version "1.7.6"
|
||||||
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.5.tgz#c3fe3677f69c12f26944563d45e7e7d818a685e4"
|
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.6.tgz#eccb135a71eba9048c68717444593a6424cffc49"
|
||||||
integrity sha512-xCD2F5+tp/SD9r5avX5bSoY8u8RH2o793xJ9Ikjz1s5qQy7cFxFbbj2c52uz3BVGhRAx/NmB57VjOquYmmxGtw==
|
integrity sha512-6c3ALO3vYDfUqNoCy8OFzq+fQ7q/ab3LCuJrmm8P04M7RmyRCCnUtJ8IzSTGbiZvyhcehGK+azNDAEJhxPV/hA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.4.4"
|
"@babel/runtime" "^7.5.5"
|
||||||
text-encoding-utf-8 "^1.0.2"
|
text-encoding-utf-8 "^1.0.2"
|
||||||
uri-js "^4.2.2"
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
|
|||||||
@ -121,7 +121,11 @@ Given('somebody reported the following posts:', table => {
|
|||||||
cy.factory()
|
cy.factory()
|
||||||
.create('User', submitter)
|
.create('User', submitter)
|
||||||
.authenticateAs(submitter)
|
.authenticateAs(submitter)
|
||||||
.create('Report', {
|
.mutate(`mutation($id: ID!, $description: String!) {
|
||||||
|
report(description: $description, id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`, {
|
||||||
id,
|
id,
|
||||||
description: 'Offensive content'
|
description: 'Offensive content'
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Given, When, Then } from "cypress-cucumber-preprocessor/steps";
|
import { Given, When, Then } from "cypress-cucumber-preprocessor/steps";
|
||||||
import helpers from "../../support/helpers";
|
import helpers from "../../support/helpers";
|
||||||
import slugify from "slug";
|
|
||||||
|
|
||||||
/* global cy */
|
/* global cy */
|
||||||
|
|
||||||
@ -11,6 +10,7 @@ let loginCredentials = {
|
|||||||
password: "1234"
|
password: "1234"
|
||||||
};
|
};
|
||||||
const narratorParams = {
|
const narratorParams = {
|
||||||
|
id: 'id-of-peter-pan',
|
||||||
name: "Peter Pan",
|
name: "Peter Pan",
|
||||||
slug: "peter-pan",
|
slug: "peter-pan",
|
||||||
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
|
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
|
||||||
@ -28,40 +28,20 @@ Given("we have a selection of categories", () => {
|
|||||||
Given("we have a selection of tags and categories as well as posts", () => {
|
Given("we have a selection of tags and categories as well as posts", () => {
|
||||||
cy.createCategories("cat12")
|
cy.createCategories("cat12")
|
||||||
.factory()
|
.factory()
|
||||||
.authenticateAs(loginCredentials)
|
|
||||||
.create("Tag", { id: "Ecology" })
|
.create("Tag", { id: "Ecology" })
|
||||||
.create("Tag", { id: "Nature" })
|
.create("Tag", { id: "Nature" })
|
||||||
.create("Tag", { id: "Democracy" });
|
.create("Tag", { id: "Democracy" });
|
||||||
const someAuthor = {
|
|
||||||
id: "authorId",
|
|
||||||
email: "author@example.org",
|
|
||||||
password: "1234"
|
|
||||||
};
|
|
||||||
const yetAnotherAuthor = {
|
|
||||||
id: "yetAnotherAuthor",
|
|
||||||
email: "yet-another-author@example.org",
|
|
||||||
password: "1234"
|
|
||||||
};
|
|
||||||
cy.factory()
|
cy.factory()
|
||||||
.create("User", someAuthor)
|
.create("User", { id: 'a1' })
|
||||||
.authenticateAs(someAuthor)
|
.create("Post", {authorId: 'a1', tagIds: [ "Ecology", "Nature", "Democracy" ], categoryIds: ["cat12"] })
|
||||||
.create("Post", { id: "p0", categoryIds: ["cat12"] })
|
.create("Post", {authorId: 'a1', tagIds: [ "Nature", "Democracy" ], categoryIds: ["cat121"] });
|
||||||
.create("Post", { id: "p1", categoryIds: ["cat121"] });
|
|
||||||
cy.factory()
|
cy.factory()
|
||||||
.create("User", yetAnotherAuthor)
|
.create("User", { id: 'a2'})
|
||||||
.authenticateAs(yetAnotherAuthor)
|
.create("Post", { authorId: 'a2', tagIds: ['Nature', 'Democracy'], categoryIds: ["cat12"] });
|
||||||
.create("Post", { id: "p2", categoryIds: ["cat12"] });
|
|
||||||
cy.factory()
|
cy.factory()
|
||||||
.authenticateAs(loginCredentials)
|
.create("Post", { authorId: narratorParams.id, tagIds: ['Democracy'], categoryIds: ["cat122"] })
|
||||||
.create("Post", { id: "p3", categoryIds: ["cat122"] })
|
|
||||||
.relate("Post", "Tags", { from: "p0", to: "Ecology" })
|
|
||||||
.relate("Post", "Tags", { from: "p0", to: "Nature" })
|
|
||||||
.relate("Post", "Tags", { from: "p0", to: "Democracy" })
|
|
||||||
.relate("Post", "Tags", { from: "p1", to: "Nature" })
|
|
||||||
.relate("Post", "Tags", { from: "p1", to: "Democracy" })
|
|
||||||
.relate("Post", "Tags", { from: "p2", to: "Nature" })
|
|
||||||
.relate("Post", "Tags", { from: "p2", to: "Democracy" })
|
|
||||||
.relate("Post", "Tags", { from: "p3", to: "Democracy" });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Given("we have the following user accounts:", table => {
|
Given("we have the following user accounts:", table => {
|
||||||
@ -158,40 +138,28 @@ When("I press {string}", label => {
|
|||||||
cy.contains(label).click();
|
cy.contains(label).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Given("we have this user in our database:", table => {
|
||||||
|
const [firstRow] = table.hashes()
|
||||||
|
cy.factory().create('User', firstRow)
|
||||||
|
})
|
||||||
|
|
||||||
Given("we have the following posts in our database:", table => {
|
Given("we have the following posts in our database:", table => {
|
||||||
table.hashes().forEach(({ Author, ...postAttributes }, i) => {
|
cy.factory().create('Category', {
|
||||||
Author = Author || `author-${i}`;
|
id: `cat-456`,
|
||||||
const userAttributes = {
|
name: "Just For Fun",
|
||||||
name: Author,
|
slug: `just-for-fun`,
|
||||||
email: `${slugify(Author, { lower: true })}@example.org`,
|
icon: "smile"
|
||||||
password: "1234"
|
})
|
||||||
};
|
|
||||||
postAttributes.deleted = Boolean(postAttributes.deleted);
|
table.hashes().forEach(({ ...postAttributes }, i) => {
|
||||||
const disabled = Boolean(postAttributes.disabled);
|
postAttributes = {
|
||||||
postAttributes.categoryIds = [`cat${i}`];
|
...postAttributes,
|
||||||
postAttributes;
|
deleted: Boolean(postAttributes.deleted),
|
||||||
cy.factory()
|
disabled: Boolean(postAttributes.disabled),
|
||||||
.create("User", userAttributes)
|
categoryIds: ['cat-456']
|
||||||
.authenticateAs(userAttributes)
|
|
||||||
.create("Category", {
|
|
||||||
id: `cat${i}`,
|
|
||||||
name: "Just For Fun",
|
|
||||||
slug: `just-for-fun-${i}`,
|
|
||||||
icon: "smile"
|
|
||||||
})
|
|
||||||
.create("Post", postAttributes);
|
|
||||||
if (disabled) {
|
|
||||||
const moderatorParams = {
|
|
||||||
email: "moderator@example.org",
|
|
||||||
role: "moderator",
|
|
||||||
password: "1234"
|
|
||||||
};
|
|
||||||
cy.factory()
|
|
||||||
.create("User", moderatorParams)
|
|
||||||
.authenticateAs(moderatorParams)
|
|
||||||
.mutate("mutation($id: ID!) { disable(id: $id) }", postAttributes);
|
|
||||||
}
|
}
|
||||||
});
|
cy.factory().create("Post", postAttributes);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
Then("I see a success message:", message => {
|
Then("I see a success message:", message => {
|
||||||
@ -210,11 +178,11 @@ When(
|
|||||||
);
|
);
|
||||||
|
|
||||||
Given("I previously created a post", () => {
|
Given("I previously created a post", () => {
|
||||||
|
lastPost.authorId = narratorParams.id
|
||||||
lastPost.title = "previously created post";
|
lastPost.title = "previously created post";
|
||||||
lastPost.content = "with some content";
|
lastPost.content = "with some content";
|
||||||
lastPost.categoryIds = "cat0";
|
lastPost.categoryIds = ["cat0"];
|
||||||
cy.factory()
|
cy.factory()
|
||||||
.authenticateAs(loginCredentials)
|
|
||||||
.create("Post", lastPost);
|
.create("Post", lastPost);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -357,7 +325,7 @@ When("mention {string} in the text", mention => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Then("the notification gets marked as read", () => {
|
Then("the notification gets marked as read", () => {
|
||||||
cy.get(".post.createdAt")
|
cy.get(".notifications-menu-popover .notification")
|
||||||
.first()
|
.first()
|
||||||
.should("have.class", "read");
|
.should("have.class", "read");
|
||||||
});
|
});
|
||||||
@ -422,11 +390,7 @@ Given("I follow the user {string}", name => {
|
|||||||
Given('"Spammy Spammer" wrote a post {string}', title => {
|
Given('"Spammy Spammer" wrote a post {string}', title => {
|
||||||
cy.createCategories("cat21")
|
cy.createCategories("cat21")
|
||||||
.factory()
|
.factory()
|
||||||
.authenticateAs({
|
.create("Post", { authorId: 'annoying-user', title, categoryIds: ["cat21"] });
|
||||||
email: "spammy-spammer@example.org",
|
|
||||||
password: "1234"
|
|
||||||
})
|
|
||||||
.create("Post", { title, categoryIds: ["cat21"] });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Then("the list of posts of this user is empty", () => {
|
Then("the list of posts of this user is empty", () => {
|
||||||
@ -445,8 +409,7 @@ Then("nobody is following the user profile anymore", () => {
|
|||||||
Given("I wrote a post {string}", title => {
|
Given("I wrote a post {string}", title => {
|
||||||
cy.createCategories(`cat213`, title)
|
cy.createCategories(`cat213`, title)
|
||||||
.factory()
|
.factory()
|
||||||
.authenticateAs(loginCredentials)
|
.create("Post", { authorId: narratorParams.id, title, categoryIds: ["cat213"] });
|
||||||
.create("Post", { title, categoryIds: ["cat213"] });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
When("I block the user {string}", name => {
|
When("I block the user {string}", name => {
|
||||||
|
|||||||
@ -8,10 +8,12 @@ Feature: Report and Moderate
|
|||||||
So I can look into it and decide what to do
|
So I can look into it and decide what to do
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
|
Given we have this user in our database:
|
||||||
|
| id | name |
|
||||||
|
| u67 | David Irving|
|
||||||
Given we have the following posts in our database:
|
Given we have the following posts in our database:
|
||||||
| Author | id | title | content |
|
| authorId | id | title | content |
|
||||||
| David Irving | p1 | The Truth about the Holocaust | It never existed! |
|
| u67 | p1 | The Truth about the Holocaust | It never existed! |
|
||||||
|
|
||||||
|
|
||||||
Scenario Outline: Report a post from various pages
|
Scenario Outline: Report a post from various pages
|
||||||
Given I am logged in with a "user" role
|
Given I am logged in with a "user" role
|
||||||
|
|||||||
@ -7,7 +7,6 @@ Feature: Block a User
|
|||||||
Given I have a user account
|
Given I have a user account
|
||||||
And there is an annoying user called "Spammy Spammer"
|
And there is an annoying user called "Spammy Spammer"
|
||||||
And I am logged in
|
And I am logged in
|
||||||
And we have a selection of categories
|
|
||||||
|
|
||||||
Scenario: Block a user
|
Scenario: Block a user
|
||||||
Given I am on the profile page of the annoying user
|
Given I am on the profile page of the annoying user
|
||||||
@ -26,6 +25,9 @@ Feature: Block a User
|
|||||||
And nobody is following the user profile anymore
|
And nobody is following the user profile anymore
|
||||||
|
|
||||||
Scenario: Posts of blocked users are filtered from search results
|
Scenario: Posts of blocked users are filtered from search results
|
||||||
|
Given we have the following posts in our database:
|
||||||
|
| id | title | content |
|
||||||
|
| im-not-blocked | Post that should be seen | cause I'm not blocked |
|
||||||
Given "Spammy Spammer" wrote a post "Spam Spam Spam"
|
Given "Spammy Spammer" wrote a post "Spam Spam Spam"
|
||||||
When I search for "Spam"
|
When I search for "Spam"
|
||||||
Then I should see the following posts in the select dropdown:
|
Then I should see the following posts in the select dropdown:
|
||||||
@ -35,3 +37,7 @@ Feature: Block a User
|
|||||||
And I refresh the page
|
And I refresh the page
|
||||||
And I search for "Spam"
|
And I search for "Spam"
|
||||||
Then the search has no results
|
Then the search has no results
|
||||||
|
But I search for "not blocked"
|
||||||
|
Then I should see the following posts in the select dropdown:
|
||||||
|
| title |
|
||||||
|
| Post that should be seen |
|
||||||
|
|||||||
@ -30,17 +30,6 @@
|
|||||||
memory: "1G"
|
memory: "1G"
|
||||||
limits:
|
limits:
|
||||||
memory: "2G"
|
memory: "2G"
|
||||||
env:
|
|
||||||
- name: NEO4J_apoc_import_file_enabled
|
|
||||||
value: "true"
|
|
||||||
- name: NEO4J_dbms_memory_pagecache_size
|
|
||||||
value: "490M"
|
|
||||||
- name: NEO4J_dbms_memory_heap_max__size
|
|
||||||
value: "500M"
|
|
||||||
- name: NEO4J_dbms_memory_heap_initial__size
|
|
||||||
value: "500M"
|
|
||||||
- name: NEO4J_dbms_security_procedures_unrestricted
|
|
||||||
value: "algo.*,apoc.*"
|
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: configmap
|
name: configmap
|
||||||
|
|||||||
@ -9,6 +9,11 @@
|
|||||||
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
|
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
|
||||||
NEO4J_AUTH: "none"
|
NEO4J_AUTH: "none"
|
||||||
CLIENT_URI: "https://nitro-staging.human-connection.org"
|
CLIENT_URI: "https://nitro-staging.human-connection.org"
|
||||||
|
NEO4J_apoc_import_file_enabled: "true"
|
||||||
|
NEO4J_dbms_memory_pagecache_size: "490M"
|
||||||
|
NEO4J_dbms_memory_heap_max__size: "500M"
|
||||||
|
NEO4J_dbms_memory_heap_initial__size: "500M"
|
||||||
|
NEO4J_dbms_security_procedures_unrestricted: "algo.*,apoc.*"
|
||||||
SENTRY_DSN_WEBAPP: ""
|
SENTRY_DSN_WEBAPP: ""
|
||||||
SENTRY_DSN_BACKEND: ""
|
SENTRY_DSN_BACKEND: ""
|
||||||
COMMIT: ""
|
COMMIT: ""
|
||||||
|
|||||||
@ -137,8 +137,8 @@ p.contentExcerpt = post.contentExcerpt,
|
|||||||
p.visibility = toLower(post.visibility),
|
p.visibility = toLower(post.visibility),
|
||||||
p.createdAt = post.createdAt.`$date`,
|
p.createdAt = post.createdAt.`$date`,
|
||||||
p.updatedAt = post.updatedAt.`$date`,
|
p.updatedAt = post.updatedAt.`$date`,
|
||||||
p.deleted = COALESCE(post.deleted,false),
|
p.deleted = COALESCE(post.deleted, false),
|
||||||
p.disabled = NOT post.isEnabled
|
p.disabled = COALESCE(NOT post.isEnabled, false)
|
||||||
WITH p, post
|
WITH p, post
|
||||||
MATCH (u:User {id: post.userId})
|
MATCH (u:User {id: post.userId})
|
||||||
MERGE (u)-[:WROTE]->(p)
|
MERGE (u)-[:WROTE]->(p)
|
||||||
|
|||||||
@ -54,6 +54,7 @@ services:
|
|||||||
- SMTP_HOST=mailserver
|
- SMTP_HOST=mailserver
|
||||||
- SMTP_PORT=25
|
- SMTP_PORT=25
|
||||||
- SMTP_IGNORE_TLS=true
|
- SMTP_IGNORE_TLS=true
|
||||||
|
- "DEBUG=${DEBUG}"
|
||||||
neo4j:
|
neo4j:
|
||||||
environment:
|
environment:
|
||||||
- NEO4J_AUTH=none
|
- NEO4J_AUTH=none
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"codecov": "^3.5.0",
|
"codecov": "^3.5.0",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.1",
|
||||||
"cypress": "^3.4.1",
|
"cypress": "^3.4.1",
|
||||||
"cypress-cucumber-preprocessor": "^1.16.0",
|
"cypress-cucumber-preprocessor": "^1.16.0",
|
||||||
"cypress-file-upload": "^3.3.3",
|
"cypress-file-upload": "^3.3.3",
|
||||||
@ -29,7 +29,7 @@
|
|||||||
"dotenv": "^8.1.0",
|
"dotenv": "^8.1.0",
|
||||||
"faker": "Marak/faker.js#master",
|
"faker": "Marak/faker.js#master",
|
||||||
"graphql-request": "^1.8.2",
|
"graphql-request": "^1.8.2",
|
||||||
"neo4j-driver": "^1.7.5",
|
"neo4j-driver": "^1.7.6",
|
||||||
"neode": "^0.3.2",
|
"neode": "^0.3.2",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"slug": "^1.1.0"
|
"slug": "^1.1.0"
|
||||||
|
|||||||
@ -83,6 +83,8 @@
|
|||||||
border-radius: $border-radius-base;
|
border-radius: $border-radius-base;
|
||||||
padding: $space-x-small $space-small;
|
padding: $space-x-small $space-small;
|
||||||
box-shadow: $box-shadow-large;
|
box-shadow: $box-shadow-large;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 73.5vh; // magic! fully visible on mobile, no scrolling on wide screen
|
||||||
}
|
}
|
||||||
|
|
||||||
@include arrow(5px, "tooltip", $background-color-inverse-soft);
|
@include arrow(5px, "tooltip", $background-color-inverse-soft);
|
||||||
|
|||||||
@ -50,6 +50,11 @@ $easeOut: cubic-bezier(0.19, 1, 0.22, 1);
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.dropdown-open {
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 15px 20px 15px 45px;
|
padding: 15px 20px 15px 45px;
|
||||||
@ -164,3 +169,9 @@ hr {
|
|||||||
.v-popover.open .trigger a {
|
.v-popover.open .trigger a {
|
||||||
color: $text-color-link-active;
|
color: $text-color-link-active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hyphenate-text {
|
||||||
|
hyphens: auto;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|||||||
@ -25,11 +25,16 @@ describe('Comment.vue', () => {
|
|||||||
success: jest.fn(),
|
success: jest.fn(),
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
},
|
},
|
||||||
|
$i18n: {
|
||||||
|
locale: () => 'en',
|
||||||
|
},
|
||||||
$filters: {
|
$filters: {
|
||||||
truncate: a => a,
|
truncate: a => a,
|
||||||
},
|
},
|
||||||
$apollo: {
|
$apollo: {
|
||||||
mutate: jest.fn().mockResolvedValue(),
|
mutate: jest.fn().mockResolvedValue({
|
||||||
|
data: { DeleteComment: { id: 'it-is-the-deleted-comment' } },
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
getters = {
|
getters = {
|
||||||
@ -113,24 +118,22 @@ describe('Comment.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('deletion of Comment from List by invoking "deleteCommentCallback()"', () => {
|
describe('deletion of Comment from List by invoking "deleteCommentCallback()"', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
wrapper.vm.deleteCommentCallback()
|
await wrapper.vm.deleteCommentCallback()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('after timeout', () => {
|
it('emits "deleteComment"', () => {
|
||||||
beforeEach(jest.runAllTimers)
|
expect(wrapper.emitted('deleteComment')).toEqual([
|
||||||
|
[{ id: 'it-is-the-deleted-comment' }],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('emits "deleteComment"', () => {
|
it('does call mutation', () => {
|
||||||
expect(wrapper.emitted().deleteComment.length).toBe(1)
|
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does call mutation', () => {
|
it('mutation is successful', () => {
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
|
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
|
||||||
})
|
|
||||||
|
|
||||||
it('mutation is successful', () => {
|
|
||||||
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -68,7 +68,6 @@ import ContentMenu from '~/components/ContentMenu'
|
|||||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||||
import HcEditCommentForm from '~/components/EditCommentForm/EditCommentForm'
|
import HcEditCommentForm from '~/components/EditCommentForm/EditCommentForm'
|
||||||
import CommentMutations from '~/graphql/CommentMutations'
|
import CommentMutations from '~/graphql/CommentMutations'
|
||||||
import PostQuery from '~/graphql/PostQuery'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: function() {
|
data: function() {
|
||||||
@ -143,26 +142,14 @@ export default {
|
|||||||
},
|
},
|
||||||
async deleteCommentCallback() {
|
async deleteCommentCallback() {
|
||||||
try {
|
try {
|
||||||
await this.$apollo.mutate({
|
const {
|
||||||
|
data: { DeleteComment },
|
||||||
|
} = await this.$apollo.mutate({
|
||||||
mutation: CommentMutations(this.$i18n).DeleteComment,
|
mutation: CommentMutations(this.$i18n).DeleteComment,
|
||||||
variables: { id: this.comment.id },
|
variables: { id: this.comment.id },
|
||||||
update: async store => {
|
|
||||||
const data = await store.readQuery({
|
|
||||||
query: PostQuery(this.$i18n),
|
|
||||||
variables: { id: this.post.id },
|
|
||||||
})
|
|
||||||
|
|
||||||
const index = data.Post[0].comments.findIndex(
|
|
||||||
deletedComment => deletedComment.id === this.comment.id,
|
|
||||||
)
|
|
||||||
if (index !== -1) {
|
|
||||||
data.Post[0].comments.splice(index, 1)
|
|
||||||
}
|
|
||||||
await store.writeQuery({ query: PostQuery(this.$i18n), data })
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
this.$toast.success(this.$t(`delete.comment.success`))
|
this.$toast.success(this.$t(`delete.comment.success`))
|
||||||
this.$emit('deleteComment')
|
this.$emit('deleteComment', DeleteComment)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$toast.error(err.message)
|
this.$toast.error(err.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,9 @@ describe('CommentForm.vue', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks = {
|
mocks = {
|
||||||
$t: jest.fn(),
|
$t: jest.fn(),
|
||||||
|
$i18n: {
|
||||||
|
locale: () => 'en',
|
||||||
|
},
|
||||||
$apollo: {
|
$apollo: {
|
||||||
mutate: jest
|
mutate: jest
|
||||||
.fn()
|
.fn()
|
||||||
|
|||||||
@ -18,11 +18,11 @@
|
|||||||
<ds-space margin-bottom="large" />
|
<ds-space margin-bottom="large" />
|
||||||
<div v-if="post.comments && post.comments.length" id="comments" class="comments">
|
<div v-if="post.comments && post.comments.length" id="comments" class="comments">
|
||||||
<comment
|
<comment
|
||||||
v-for="(comment, index) in post.comments"
|
v-for="comment in post.comments"
|
||||||
:key="comment.id"
|
:key="comment.id"
|
||||||
:comment="comment"
|
:comment="comment"
|
||||||
:post="post"
|
:post="post"
|
||||||
@deleteComment="post.comments.splice(index, 1)"
|
@deleteComment="deleteComment"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hc-empty v-else name="empty" icon="messages" />
|
<hc-empty v-else name="empty" icon="messages" />
|
||||||
@ -40,5 +40,12 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
post: { type: Object, default: () => {} },
|
post: { type: Object, default: () => {} },
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
deleteComment(deleted) {
|
||||||
|
this.post.comments = this.post.comments.map(comment => {
|
||||||
|
return comment.id === deleted.id ? deleted : comment
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export default {
|
|||||||
this.disabled = true
|
this.disabled = true
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: CommentMutations().UpdateComment,
|
mutation: CommentMutations(this.$i18n).UpdateComment,
|
||||||
variables: {
|
variables: {
|
||||||
content: this.form.content,
|
content: this.form.content,
|
||||||
id: this.comment.id,
|
id: this.comment.id,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import Editor from './Editor'
|
|||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import Styleguide from '@human-connection/styleguide'
|
import Styleguide from '@human-connection/styleguide'
|
||||||
import MutationObserver from 'mutation-observer'
|
import MutationObserver from 'mutation-observer'
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
global.MutationObserver = MutationObserver
|
global.MutationObserver = MutationObserver
|
||||||
|
|
||||||
@ -55,9 +56,11 @@ describe('Editor.vue', () => {
|
|||||||
propsData.value = 'I am a piece of text'
|
propsData.value = 'I am a piece of text'
|
||||||
})
|
})
|
||||||
|
|
||||||
it.skip('renders', () => {
|
it('renders', async () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.find('.ProseMirror').text()).toContain('I am a piece of text')
|
await Vue.nextTick().then(() => {
|
||||||
|
expect(wrapper.find('.editor-content').text()).toContain(propsData.value)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -88,6 +91,29 @@ describe('Editor.vue', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('limists suggestion list to 15 users', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
let manyUsersList = []
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
manyUsersList.push({ id: `user${i}` })
|
||||||
|
}
|
||||||
|
propsData.users = manyUsersList
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when query is empty', () => {
|
||||||
|
expect(
|
||||||
|
wrapper.vm.editor.extensions.options.mention.onFilter(propsData.users),
|
||||||
|
).toHaveLength(15)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when query is present', () => {
|
||||||
|
expect(
|
||||||
|
wrapper.vm.editor.extensions.options.mention.onFilter(propsData.users, 'user'),
|
||||||
|
).toHaveLength(15)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('sets the Hashtag items to the hashtags', () => {
|
it('sets the Hashtag items to the hashtags', () => {
|
||||||
propsData.hashtags = [
|
propsData.hashtags = [
|
||||||
{
|
{
|
||||||
@ -105,6 +131,29 @@ describe('Editor.vue', () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('limists suggestion list to 15 hashtags', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
let manyHashtagsList = []
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
manyHashtagsList.push({ id: `hashtag${i}` })
|
||||||
|
}
|
||||||
|
propsData.hashtags = manyHashtagsList
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when query is empty', () => {
|
||||||
|
expect(
|
||||||
|
wrapper.vm.editor.extensions.options.hashtag.onFilter(propsData.hashtags),
|
||||||
|
).toHaveLength(15)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when query is present', () => {
|
||||||
|
expect(
|
||||||
|
wrapper.vm.editor.extensions.options.hashtag.onFilter(propsData.hashtags, 'hashtag'),
|
||||||
|
).toHaveLength(15)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -204,12 +204,14 @@ export default {
|
|||||||
filterSuggestionList(items, query) {
|
filterSuggestionList(items, query) {
|
||||||
query = this.sanitizeQuery(query)
|
query = this.sanitizeQuery(query)
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return items
|
return items.slice(0, 15)
|
||||||
}
|
}
|
||||||
return items.filter(item => {
|
|
||||||
|
const filteredList = items.filter(item => {
|
||||||
const itemString = item.slug || item.id
|
const itemString = item.slug || item.id
|
||||||
return itemString.toLowerCase().includes(query.toLowerCase())
|
return itemString.toLowerCase().includes(query.toLowerCase())
|
||||||
})
|
})
|
||||||
|
return filteredList.slice(0, 15)
|
||||||
},
|
},
|
||||||
sanitizeQuery(query) {
|
sanitizeQuery(query) {
|
||||||
if (this.suggestionType === HASHTAG) {
|
if (this.suggestionType === HASHTAG) {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const post = {
|
|||||||
],
|
],
|
||||||
__typename: 'User',
|
__typename: 'User',
|
||||||
},
|
},
|
||||||
commentedCount: 12,
|
commentsCount: 12,
|
||||||
categories: [],
|
categories: [],
|
||||||
shoutedCount: 421,
|
shoutedCount: 421,
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
|
|||||||
@ -50,7 +50,9 @@ describe('PostCard', () => {
|
|||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
},
|
},
|
||||||
$apollo: {
|
$apollo: {
|
||||||
mutate: jest.fn().mockResolvedValue(),
|
mutate: jest.fn().mockResolvedValue({
|
||||||
|
data: { DeletePost: { id: 'deleted-post-id' } },
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
getters = {
|
getters = {
|
||||||
@ -94,7 +96,7 @@ describe('PostCard', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('emits "removePostFromList"', () => {
|
it('emits "removePostFromList"', () => {
|
||||||
expect(wrapper.emitted().removePostFromList).toHaveLength(1)
|
expect(wrapper.emitted('removePostFromList')).toEqual([[{ id: 'deleted-post-id' }]])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -20,12 +20,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<ds-space margin-bottom="small" />
|
<ds-space margin-bottom="small" />
|
||||||
<!-- Post Title -->
|
<!-- Post Title -->
|
||||||
<ds-heading tag="h3" no-margin>{{ post.title }}</ds-heading>
|
<ds-heading tag="h3" no-margin class="hyphenate-text">{{ post.title }}</ds-heading>
|
||||||
<ds-space margin-bottom="small" />
|
<ds-space margin-bottom="small" />
|
||||||
<!-- Post Content Excerpt -->
|
<!-- Post Content Excerpt -->
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<!-- TODO: replace editor content with tiptap render view -->
|
<!-- TODO: replace editor content with tiptap render view -->
|
||||||
<div class="hc-editor-content" v-html="excerpt" />
|
<div class="hc-editor-content hyphenate-text" v-html="excerpt" />
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
<!-- Footer o the Post -->
|
<!-- Footer o the Post -->
|
||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
@ -51,9 +51,9 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Comments Count -->
|
<!-- Comments Count -->
|
||||||
<span :style="{ opacity: post.commentedCount ? 1 : 0.5 }">
|
<span :style="{ opacity: post.commentsCount ? 1 : 0.5 }">
|
||||||
<ds-icon name="comments" />
|
<ds-icon name="comments" />
|
||||||
<small>{{ post.commentedCount }}</small>
|
<small>{{ post.commentsCount }}</small>
|
||||||
</span>
|
</span>
|
||||||
<!-- Menu -->
|
<!-- Menu -->
|
||||||
<content-menu
|
<content-menu
|
||||||
@ -118,9 +118,11 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async deletePostCallback() {
|
async deletePostCallback() {
|
||||||
try {
|
try {
|
||||||
await this.$apollo.mutate(deletePostMutation(this.post.id))
|
const {
|
||||||
|
data: { DeletePost },
|
||||||
|
} = await this.$apollo.mutate(deletePostMutation(this.post.id))
|
||||||
this.$toast.success(this.$t('delete.contribution.success'))
|
this.$toast.success(this.$t('delete.contribution.success'))
|
||||||
this.$emit('removePostFromList')
|
this.$emit('removePostFromList', DeletePost)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$toast.error(err.message)
|
this.$toast.error(err.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -122,7 +122,7 @@ describe('SearchInput.vue', () => {
|
|||||||
name: 'Trick',
|
name: 'Trick',
|
||||||
slug: 'trick',
|
slug: 'trick',
|
||||||
},
|
},
|
||||||
commentedCount: 0,
|
commentsCount: 0,
|
||||||
createdAt: '2019-03-13T11:00:20.835Z',
|
createdAt: '2019-03-13T11:00:20.835Z',
|
||||||
id: 'p10',
|
id: 'p10',
|
||||||
label: 'Eos aut illo omnis quis eaque et iure aut.',
|
label: 'Eos aut illo omnis quis eaque et iure aut.',
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
<ds-flex-item>
|
<ds-flex-item>
|
||||||
<ds-text size="small" color="softer" class="search-meta">
|
<ds-text size="small" color="softer" class="search-meta">
|
||||||
<span style="text-align: right;">
|
<span style="text-align: right;">
|
||||||
<b>{{ option.commentedCount }}</b>
|
<b>{{ option.commentsCount }}</b>
|
||||||
<ds-icon name="comments" />
|
<ds-icon name="comments" />
|
||||||
</span>
|
</span>
|
||||||
<span style="width: 36px; display: inline-block; text-align: right;">
|
<span style="width: 36px; display: inline-block; text-align: right;">
|
||||||
|
|||||||
@ -37,9 +37,9 @@ describe('Notification', () => {
|
|||||||
describe('given a notification about a comment on a post', () => {
|
describe('given a notification about a comment on a post', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
propsData.notification = {
|
propsData.notification = {
|
||||||
reason: 'comment_on_post',
|
reason: 'commented_on_post',
|
||||||
post: null,
|
from: {
|
||||||
comment: {
|
__typename: 'Comment',
|
||||||
id: 'comment-1',
|
id: 'comment-1',
|
||||||
contentExcerpt:
|
contentExcerpt:
|
||||||
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
|
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
|
||||||
@ -56,7 +56,7 @@ describe('Notification', () => {
|
|||||||
it('renders reason', () => {
|
it('renders reason', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||||
'notifications.menu.comment_on_post',
|
'notifications.menu.commented_on_post',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
it('renders title', () => {
|
it('renders title', () => {
|
||||||
@ -92,14 +92,14 @@ describe('Notification', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
propsData.notification = {
|
propsData.notification = {
|
||||||
reason: 'mentioned_in_post',
|
reason: 'mentioned_in_post',
|
||||||
post: {
|
from: {
|
||||||
|
__typename: 'Post',
|
||||||
title: "It's a post title",
|
title: "It's a post title",
|
||||||
id: 'post-1',
|
id: 'post-1',
|
||||||
slug: 'its-a-title',
|
slug: 'its-a-title',
|
||||||
contentExcerpt:
|
contentExcerpt:
|
||||||
'<a href="/profile/u3" target="_blank">@jenny-rostock</a> is the best on this post.',
|
'<a href="/profile/u3" target="_blank">@jenny-rostock</a> is the best on this post.',
|
||||||
},
|
},
|
||||||
comment: null,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -138,8 +138,8 @@ describe('Notification', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
propsData.notification = {
|
propsData.notification = {
|
||||||
reason: 'mentioned_in_comment',
|
reason: 'mentioned_in_comment',
|
||||||
post: null,
|
from: {
|
||||||
comment: {
|
__typename: 'Comment',
|
||||||
id: 'comment-1',
|
id: 'comment-1',
|
||||||
contentExcerpt:
|
contentExcerpt:
|
||||||
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
|
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
|
||||||
|
|||||||
@ -1,14 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-space :class="[{ read: notification.read }, notification]" margin-bottom="x-small">
|
<ds-space :class="{ read: notification.read, notification: true }" margin-bottom="x-small">
|
||||||
<client-only>
|
<client-only>
|
||||||
<ds-space margin-bottom="x-small">
|
<ds-space margin-bottom="x-small">
|
||||||
<hc-user
|
<hc-user :user="from.author" :date-time="from.createdAt" :trunc="35" />
|
||||||
v-if="resourceType == 'Post'"
|
|
||||||
:user="post.author"
|
|
||||||
:date-time="post.createdAt"
|
|
||||||
:trunc="35"
|
|
||||||
/>
|
|
||||||
<hc-user v-else :user="comment.author" :date-time="comment.createdAt" :trunc="35" />
|
|
||||||
</ds-space>
|
</ds-space>
|
||||||
<ds-text class="reason-text-for-test" color="soft">
|
<ds-text class="reason-text-for-test" color="soft">
|
||||||
{{ $t(`notifications.menu.${notification.reason}`) }}
|
{{ $t(`notifications.menu.${notification.reason}`) }}
|
||||||
@ -22,16 +16,15 @@
|
|||||||
>
|
>
|
||||||
<ds-space margin-bottom="x-small">
|
<ds-space margin-bottom="x-small">
|
||||||
<ds-card
|
<ds-card
|
||||||
:header="post.title || comment.post.title"
|
:header="from.title || from.post.title"
|
||||||
hover
|
hover
|
||||||
space="x-small"
|
space="x-small"
|
||||||
class="notifications-card"
|
class="notifications-card"
|
||||||
>
|
>
|
||||||
<ds-space margin-bottom="x-small" />
|
<ds-space margin-bottom="x-small" />
|
||||||
<div v-if="resourceType == 'Post'">{{ post.contentExcerpt | removeHtml }}</div>
|
<div>
|
||||||
<div v-else>
|
<span v-if="isComment" class="comment-notification-header">Comment:</span>
|
||||||
<span class="comment-notification-header">Comment:</span>
|
{{ from.contentExcerpt | removeHtml }}
|
||||||
{{ comment.contentExcerpt | removeHtml }}
|
|
||||||
</div>
|
</div>
|
||||||
</ds-card>
|
</ds-card>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
@ -54,23 +47,21 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
resourceType() {
|
from() {
|
||||||
return this.post.id ? 'Post' : 'Comment'
|
return this.notification.from
|
||||||
},
|
},
|
||||||
post() {
|
isComment() {
|
||||||
return this.notification.post || {}
|
return this.from.__typename === 'Comment'
|
||||||
},
|
|
||||||
comment() {
|
|
||||||
return this.notification.comment || {}
|
|
||||||
},
|
},
|
||||||
params() {
|
params() {
|
||||||
|
const post = this.isComment ? this.from.post : this.from
|
||||||
return {
|
return {
|
||||||
id: this.post.id || this.comment.post.id,
|
id: post.id,
|
||||||
slug: this.post.slug || this.comment.post.slug,
|
slug: post.slug,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hashParam() {
|
hashParam() {
|
||||||
return this.post.id ? {} : { hash: `#commentId-${this.comment.id}` }
|
return this.isComment ? { hash: `#commentId-${this.from.id}` } : {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,9 +40,9 @@ describe('NotificationList.vue', () => {
|
|||||||
propsData = {
|
propsData = {
|
||||||
notifications: [
|
notifications: [
|
||||||
{
|
{
|
||||||
id: 'notification-41',
|
|
||||||
read: false,
|
read: false,
|
||||||
post: {
|
from: {
|
||||||
|
__typename: 'Post',
|
||||||
id: 'post-1',
|
id: 'post-1',
|
||||||
title: 'some post title',
|
title: 'some post title',
|
||||||
slug: 'some-post-title',
|
slug: 'some-post-title',
|
||||||
@ -55,9 +55,9 @@ describe('NotificationList.vue', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'notification-42',
|
|
||||||
read: false,
|
read: false,
|
||||||
post: {
|
from: {
|
||||||
|
__typename: 'Post',
|
||||||
id: 'post-2',
|
id: 'post-2',
|
||||||
title: 'another post title',
|
title: 'another post title',
|
||||||
slug: 'another-post-title',
|
slug: 'another-post-title',
|
||||||
@ -115,9 +115,9 @@ describe('NotificationList.vue', () => {
|
|||||||
.trigger('click')
|
.trigger('click')
|
||||||
})
|
})
|
||||||
|
|
||||||
it("emits 'markAsRead' with the notificationId", () => {
|
it("emits 'markAsRead' with the id of the notification source", () => {
|
||||||
expect(wrapper.emitted('markAsRead')).toBeTruthy()
|
expect(wrapper.emitted('markAsRead')).toBeTruthy()
|
||||||
expect(wrapper.emitted('markAsRead')[0]).toEqual(['notification-42'])
|
expect(wrapper.emitted('markAsRead')[0]).toEqual(['post-2'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
v-for="notification in notifications"
|
v-for="notification in notifications"
|
||||||
:key="notification.id"
|
:key="notification.id"
|
||||||
:notification="notification"
|
:notification="notification"
|
||||||
@read="markAsRead(notification.id)"
|
@read="markAsRead(notification.from.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -24,8 +24,8 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
markAsRead(notificationId) {
|
markAsRead(notificationSourceId) {
|
||||||
this.$emit('markAsRead', notificationId)
|
this.$emit('markAsRead', notificationSourceId)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Dropdown from '~/components/Dropdown'
|
import Dropdown from '~/components/Dropdown'
|
||||||
import { currentUserNotificationsQuery, updateNotificationMutation } from '~/graphql/User'
|
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
|
||||||
import NotificationList from '../NotificationList/NotificationList'
|
import NotificationList from '../NotificationList/NotificationList'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -27,35 +27,42 @@ export default {
|
|||||||
NotificationList,
|
NotificationList,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
notifications: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
placement: { type: String },
|
placement: { type: String },
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
totalNotifications() {
|
|
||||||
return (this.notifications || []).length
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
async markAsRead(notificationId) {
|
async markAsRead(notificationSourceId) {
|
||||||
const variables = { id: notificationId, read: true }
|
const variables = { id: notificationSourceId }
|
||||||
try {
|
try {
|
||||||
await this.$apollo.mutate({
|
const {
|
||||||
mutation: updateNotificationMutation(),
|
data: { markAsRead },
|
||||||
|
} = await this.$apollo.mutate({
|
||||||
|
mutation: markAsReadMutation(this.$i18n),
|
||||||
variables,
|
variables,
|
||||||
})
|
})
|
||||||
|
if (!(markAsRead && markAsRead.read === true)) return
|
||||||
|
this.notifications = this.notifications.map(n => {
|
||||||
|
return n.from.id === markAsRead.from.id ? markAsRead : n
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(err)
|
throw new Error(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
totalNotifications() {
|
||||||
|
return (this.notifications || []).length
|
||||||
|
},
|
||||||
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
notifications: {
|
notifications: {
|
||||||
query: currentUserNotificationsQuery(),
|
query() {
|
||||||
update: data => {
|
return notificationQuery(this.$i18n)
|
||||||
const {
|
|
||||||
currentUser: { notifications },
|
|
||||||
} = data
|
|
||||||
return notifications
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
export default i18n => {
|
export default i18n => {
|
||||||
|
const lang = i18n.locale().toUpperCase()
|
||||||
return {
|
return {
|
||||||
CreateComment: gql`
|
CreateComment: gql`
|
||||||
mutation($postId: ID!, $content: String!) {
|
mutation($postId: ID!, $content: String!) {
|
||||||
@ -55,6 +56,31 @@ export default i18n => {
|
|||||||
mutation($id: ID!) {
|
mutation($id: ID!) {
|
||||||
DeleteComment(id: $id) {
|
DeleteComment(id: $id) {
|
||||||
id
|
id
|
||||||
|
contentExcerpt
|
||||||
|
content
|
||||||
|
createdAt
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
shoutedCount
|
||||||
|
contributionsCount
|
||||||
|
commentedCount
|
||||||
|
followedByCount
|
||||||
|
followedByCurrentUser
|
||||||
|
location {
|
||||||
|
name: name${lang}
|
||||||
|
}
|
||||||
|
badges {
|
||||||
|
id
|
||||||
|
icon
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
74
webapp/graphql/Fragments.js
Normal file
74
webapp/graphql/Fragments.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const userFragment = lang => gql`
|
||||||
|
fragment user on User {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
shoutedCount
|
||||||
|
contributionsCount
|
||||||
|
commentedCount
|
||||||
|
followedByCount
|
||||||
|
followedByCurrentUser
|
||||||
|
location {
|
||||||
|
name: name${lang}
|
||||||
|
}
|
||||||
|
badges {
|
||||||
|
id
|
||||||
|
icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const postCountsFragment = gql`
|
||||||
|
fragment postCounts on Post {
|
||||||
|
commentsCount
|
||||||
|
shoutedCount
|
||||||
|
shoutedByCurrentUser
|
||||||
|
emotionsCount
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export const postFragment = lang => gql`
|
||||||
|
${userFragment(lang)}
|
||||||
|
|
||||||
|
fragment post on Post {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
content
|
||||||
|
contentExcerpt
|
||||||
|
createdAt
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
slug
|
||||||
|
image
|
||||||
|
author {
|
||||||
|
...user
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export const commentFragment = lang => gql`
|
||||||
|
${userFragment(lang)}
|
||||||
|
|
||||||
|
fragment comment on Comment {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
content
|
||||||
|
contentExcerpt
|
||||||
|
author {
|
||||||
|
...user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -1,77 +1,20 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
import { postFragment, commentFragment, postCountsFragment } from './Fragments'
|
||||||
|
|
||||||
export default i18n => {
|
export default i18n => {
|
||||||
const lang = i18n.locale().toUpperCase()
|
const lang = i18n.locale().toUpperCase()
|
||||||
return gql`
|
return gql`
|
||||||
|
${postFragment(lang)}
|
||||||
|
${postCountsFragment}
|
||||||
|
${commentFragment(lang)}
|
||||||
|
|
||||||
query Post($id: ID!) {
|
query Post($id: ID!) {
|
||||||
Post(id: $id) {
|
Post(id: $id) {
|
||||||
id
|
...post
|
||||||
title
|
...postCounts
|
||||||
content
|
|
||||||
createdAt
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
slug
|
|
||||||
image
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
name
|
|
||||||
avatar
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
shoutedCount
|
|
||||||
contributionsCount
|
|
||||||
commentedCount
|
|
||||||
followedByCount
|
|
||||||
followedByCurrentUser
|
|
||||||
location {
|
|
||||||
name: name${lang}
|
|
||||||
}
|
|
||||||
badges {
|
|
||||||
id
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tags {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
comments(orderBy: createdAt_asc) {
|
comments(orderBy: createdAt_asc) {
|
||||||
id
|
...comment
|
||||||
contentExcerpt
|
|
||||||
content
|
|
||||||
createdAt
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
name
|
|
||||||
avatar
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
shoutedCount
|
|
||||||
contributionsCount
|
|
||||||
commentedCount
|
|
||||||
followedByCount
|
|
||||||
followedByCurrentUser
|
|
||||||
location {
|
|
||||||
name: name${lang}
|
|
||||||
}
|
|
||||||
badges {
|
|
||||||
id
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
categories {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
shoutedCount
|
|
||||||
shoutedByCurrentUser
|
|
||||||
emotionsCount
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -80,45 +23,16 @@ export default i18n => {
|
|||||||
export const filterPosts = i18n => {
|
export const filterPosts = i18n => {
|
||||||
const lang = i18n.locale().toUpperCase()
|
const lang = i18n.locale().toUpperCase()
|
||||||
return gql`
|
return gql`
|
||||||
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
|
${postFragment(lang)}
|
||||||
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
|
${postCountsFragment}
|
||||||
id
|
|
||||||
title
|
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
|
||||||
contentExcerpt
|
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
|
||||||
createdAt
|
...post
|
||||||
disabled
|
...postCounts
|
||||||
deleted
|
|
||||||
slug
|
|
||||||
image
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
avatar
|
|
||||||
slug
|
|
||||||
name
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
contributionsCount
|
|
||||||
shoutedCount
|
|
||||||
commentedCount
|
|
||||||
followedByCount
|
|
||||||
followedByCurrentUser
|
|
||||||
location {
|
|
||||||
name: name${lang}
|
|
||||||
}
|
|
||||||
badges {
|
|
||||||
id
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
categories {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
shoutedCount
|
|
||||||
}
|
}
|
||||||
}
|
`
|
||||||
`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PostsEmotionsByCurrentUser = () => {
|
export const PostsEmotionsByCurrentUser = () => {
|
||||||
@ -128,3 +42,22 @@ export const PostsEmotionsByCurrentUser = () => {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const relatedContributions = i18n => {
|
||||||
|
const lang = i18n.locale().toUpperCase()
|
||||||
|
return gql`
|
||||||
|
${postFragment(lang)}
|
||||||
|
${postCountsFragment}
|
||||||
|
|
||||||
|
query Post($slug: String!) {
|
||||||
|
Post(slug: $slug) {
|
||||||
|
...post
|
||||||
|
...postCounts
|
||||||
|
relatedContributions(first: 2) {
|
||||||
|
...post
|
||||||
|
...postCounts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
import { postFragment, commentFragment } from './Fragments'
|
||||||
|
|
||||||
export default i18n => {
|
export default i18n => {
|
||||||
const lang = i18n.locale().toUpperCase()
|
const lang = i18n.locale().toUpperCase()
|
||||||
@ -76,63 +77,26 @@ export default i18n => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const currentUserNotificationsQuery = () => {
|
export const notificationQuery = i18n => {
|
||||||
|
const lang = i18n.locale().toUpperCase()
|
||||||
return gql`
|
return gql`
|
||||||
|
${commentFragment(lang)}
|
||||||
|
${postFragment(lang)}
|
||||||
|
|
||||||
query {
|
query {
|
||||||
currentUser {
|
notifications(read: false, orderBy: createdAt_desc) {
|
||||||
id
|
read
|
||||||
notifications(read: false, orderBy: createdAt_desc) {
|
reason
|
||||||
id
|
createdAt
|
||||||
read
|
from {
|
||||||
reason
|
__typename
|
||||||
createdAt
|
... on Post {
|
||||||
post {
|
...post
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
title
|
|
||||||
contentExcerpt
|
|
||||||
slug
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
name
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
avatar
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
comment {
|
... on Comment {
|
||||||
id
|
...comment
|
||||||
createdAt
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
contentExcerpt
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
name
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
avatar
|
|
||||||
}
|
|
||||||
post {
|
post {
|
||||||
id
|
...post
|
||||||
createdAt
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
title
|
|
||||||
contentExcerpt
|
|
||||||
slug
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
name
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
avatar
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,12 +105,29 @@ export const currentUserNotificationsQuery = () => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateNotificationMutation = () => {
|
export const markAsReadMutation = i18n => {
|
||||||
|
const lang = i18n.locale().toUpperCase()
|
||||||
return gql`
|
return gql`
|
||||||
mutation($id: ID!, $read: Boolean!) {
|
${commentFragment(lang)}
|
||||||
UpdateNotification(id: $id, read: $read) {
|
${postFragment(lang)}
|
||||||
id
|
|
||||||
|
mutation($id: ID!) {
|
||||||
|
markAsRead(id: $id) {
|
||||||
read
|
read
|
||||||
|
reason
|
||||||
|
createdAt
|
||||||
|
from {
|
||||||
|
__typename
|
||||||
|
... on Post {
|
||||||
|
...post
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
...comment
|
||||||
|
post {
|
||||||
|
...post
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -121,7 +121,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ds-container>
|
</ds-container>
|
||||||
</div>
|
</div>
|
||||||
<ds-container style="word-break: break-all">
|
<ds-container>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<nuxt />
|
<nuxt />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -134,7 +134,7 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …",
|
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …",
|
||||||
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
|
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
|
||||||
"comment_on_post": "Hat deinen Beitrag kommentiert …"
|
"commented_on_post": "Hat deinen Beitrag kommentiert …"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@ -134,7 +134,7 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"mentioned_in_post": "Mentioned you in a post …",
|
"mentioned_in_post": "Mentioned you in a post …",
|
||||||
"mentioned_in_comment": "Mentioned you in a comment …",
|
"mentioned_in_comment": "Mentioned you in a comment …",
|
||||||
"comment_on_post": "Commented on your post …"
|
"commented_on_post": "Commented on your post …"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@ -60,10 +60,10 @@
|
|||||||
"apollo-cache-inmemory": "~1.6.3",
|
"apollo-cache-inmemory": "~1.6.3",
|
||||||
"apollo-client": "~2.6.4",
|
"apollo-client": "~2.6.4",
|
||||||
"cookie-universal-nuxt": "~2.0.17",
|
"cookie-universal-nuxt": "~2.0.17",
|
||||||
"cross-env": "~5.2.0",
|
"cross-env": "~5.2.1",
|
||||||
"date-fns": "2.0.1",
|
"date-fns": "2.0.1",
|
||||||
"express": "~4.17.1",
|
"express": "~4.17.1",
|
||||||
"graphql": "~14.5.3",
|
"graphql": "~14.5.4",
|
||||||
"isemail": "^3.2.0",
|
"isemail": "^3.2.0",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"linkify-it": "~2.2.0",
|
"linkify-it": "~2.2.0",
|
||||||
@ -78,7 +78,7 @@
|
|||||||
"v-tooltip": "~2.0.2",
|
"v-tooltip": "~2.0.2",
|
||||||
"vue-count-to": "~1.0.13",
|
"vue-count-to": "~1.0.13",
|
||||||
"vue-infinite-scroll": "^2.0.2",
|
"vue-infinite-scroll": "^2.0.2",
|
||||||
"vue-izitoast": "^1.2.0",
|
"vue-izitoast": "^1.2.1",
|
||||||
"vue-sweetalert-icons": "~4.2.0",
|
"vue-sweetalert-icons": "~4.2.0",
|
||||||
"vuex-i18n": "~1.13.1",
|
"vuex-i18n": "~1.13.1",
|
||||||
"xregexp": "^4.2.4",
|
"xregexp": "^4.2.4",
|
||||||
@ -107,8 +107,8 @@
|
|||||||
"eslint-config-standard": "~12.0.0",
|
"eslint-config-standard": "~12.0.0",
|
||||||
"eslint-loader": "~3.0.0",
|
"eslint-loader": "~3.0.0",
|
||||||
"eslint-plugin-import": "~2.18.2",
|
"eslint-plugin-import": "~2.18.2",
|
||||||
"eslint-plugin-jest": "~22.15.2",
|
"eslint-plugin-jest": "~22.16.0",
|
||||||
"eslint-plugin-node": "~9.1.0",
|
"eslint-plugin-node": "~9.2.0",
|
||||||
"eslint-plugin-prettier": "~3.1.0",
|
"eslint-plugin-prettier": "~3.1.0",
|
||||||
"eslint-plugin-promise": "~4.2.1",
|
"eslint-plugin-promise": "~4.2.1",
|
||||||
"eslint-plugin-standard": "~4.0.1",
|
"eslint-plugin-standard": "~4.0.1",
|
||||||
@ -120,7 +120,7 @@
|
|||||||
"node-sass": "~4.12.0",
|
"node-sass": "~4.12.0",
|
||||||
"nodemon": "~1.19.1",
|
"nodemon": "~1.19.1",
|
||||||
"prettier": "~1.18.2",
|
"prettier": "~1.18.2",
|
||||||
"sass-loader": "~7.3.1",
|
"sass-loader": "~8.0.0",
|
||||||
"style-loader": "~0.23.1",
|
"style-loader": "~0.23.1",
|
||||||
"style-resources-loader": "~1.2.1",
|
"style-resources-loader": "~1.2.1",
|
||||||
"tippy.js": "^4.3.5",
|
"tippy.js": "^4.3.5",
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
<hc-post-card
|
<hc-post-card
|
||||||
:post="post"
|
:post="post"
|
||||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||||
@removePostFromList="deletePost(index, post.id)"
|
@removePostFromList="deletePost"
|
||||||
/>
|
/>
|
||||||
</masonry-grid-item>
|
</masonry-grid-item>
|
||||||
</template>
|
</template>
|
||||||
@ -115,7 +115,7 @@ export default {
|
|||||||
label: this.$t('sorting.commented'),
|
label: this.$t('sorting.commented'),
|
||||||
value: 'Commented',
|
value: 'Commented',
|
||||||
icons: 'comment',
|
icons: 'comment',
|
||||||
order: 'commentedCount_desc',
|
order: 'commentsCount_desc',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@ -164,9 +164,9 @@ export default {
|
|||||||
showMoreContributions() {
|
showMoreContributions() {
|
||||||
this.offset += this.pageSize
|
this.offset += this.pageSize
|
||||||
},
|
},
|
||||||
deletePost(_index, postId) {
|
deletePost(deletedPost) {
|
||||||
this.posts = this.posts.filter(post => {
|
this.posts = this.posts.filter(post => {
|
||||||
return post.id !== postId
|
return post.id !== deletedPost.id
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -185,10 +185,8 @@ export default {
|
|||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
update({ Post }) {
|
update({ Post }) {
|
||||||
// TODO: find out why `update` gets called twice initially.
|
this.hasMore = Post && Post.length >= this.pageSize
|
||||||
// We have to filter for uniq posts only because we get the same
|
if (!Post) return
|
||||||
// result set twice.
|
|
||||||
this.hasMore = Post.length >= this.pageSize
|
|
||||||
const posts = uniqBy([...this.posts, ...Post], 'id')
|
const posts = uniqBy([...this.posts, ...Post], 'id')
|
||||||
this.posts = posts
|
this.posts = posts
|
||||||
},
|
},
|
||||||
|
|||||||
@ -18,9 +18,9 @@
|
|||||||
/>
|
/>
|
||||||
</client-only>
|
</client-only>
|
||||||
<ds-space margin-bottom="small" />
|
<ds-space margin-bottom="small" />
|
||||||
<ds-heading tag="h3" no-margin>{{ post.title }}</ds-heading>
|
<ds-heading tag="h3" no-margin class="hyphenate-text">{{ post.title }}</ds-heading>
|
||||||
<ds-space margin-bottom="small" />
|
<ds-space margin-bottom="small" />
|
||||||
<content-viewer class="content" :content="post.content" />
|
<content-viewer class="content hyphenate-text" :content="post.content" />
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
<ds-space margin="xx-large" />
|
<ds-space margin="xx-large" />
|
||||||
<!-- Categories -->
|
<!-- Categories -->
|
||||||
|
|||||||
@ -36,11 +36,11 @@
|
|||||||
<ds-section style="margin: 0 -1.5rem; padding: 1.5rem;">
|
<ds-section style="margin: 0 -1.5rem; padding: 1.5rem;">
|
||||||
<ds-flex v-if="post.relatedContributions && post.relatedContributions.length" gutter="small">
|
<ds-flex v-if="post.relatedContributions && post.relatedContributions.length" gutter="small">
|
||||||
<hc-post-card
|
<hc-post-card
|
||||||
v-for="(relatedPost, index) in post.relatedContributions"
|
v-for="relatedPost in post.relatedContributions"
|
||||||
:key="relatedPost.id"
|
:key="relatedPost.id"
|
||||||
:post="relatedPost"
|
:post="relatedPost"
|
||||||
:width="{ base: '100%', lg: 1 }"
|
:width="{ base: '100%', lg: 1 }"
|
||||||
@removePostFromList="post.relatedContributions.splice(index, 1)"
|
@removePostFromList="removePostFromList"
|
||||||
/>
|
/>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
<hc-empty v-else margin="large" icon="file" message="No related Posts" />
|
<hc-empty v-else margin="large" icon="file" message="No related Posts" />
|
||||||
@ -50,9 +50,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import gql from 'graphql-tag'
|
|
||||||
import HcPostCard from '~/components/PostCard'
|
import HcPostCard from '~/components/PostCard'
|
||||||
import HcEmpty from '~/components/Empty.vue'
|
import HcEmpty from '~/components/Empty.vue'
|
||||||
|
import { relatedContributions } from '~/graphql/PostQuery'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
transition: {
|
transition: {
|
||||||
@ -68,57 +68,17 @@ export default {
|
|||||||
return this.Post ? this.Post[0] || {} : {}
|
return this.Post ? this.Post[0] || {} : {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
removePostFromList(deletedPost) {
|
||||||
|
this.post.relatedContributions = this.post.relatedContributions.filter(contribution => {
|
||||||
|
return contribution.id !== deletedPost.id
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
Post: {
|
Post: {
|
||||||
query() {
|
query() {
|
||||||
return gql`
|
return relatedContributions(this.$i18n)
|
||||||
query Post($slug: String!) {
|
|
||||||
Post(slug: $slug) {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
tags {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
categories {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
relatedContributions(first: 2) {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
slug
|
|
||||||
contentExcerpt
|
|
||||||
shoutedCount
|
|
||||||
commentedCount
|
|
||||||
categories {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
avatar
|
|
||||||
contributionsCount
|
|
||||||
followedByCount
|
|
||||||
followedByCurrentUser
|
|
||||||
commentedCount
|
|
||||||
location {
|
|
||||||
name: name${this.$i18n.locale().toUpperCase()}
|
|
||||||
}
|
|
||||||
badges {
|
|
||||||
id
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
shoutedCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
},
|
},
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -104,9 +104,7 @@ describe('ProfileSlug', () => {
|
|||||||
|
|
||||||
describe('currently no posts available (e.g. after tab switching)', () => {
|
describe('currently no posts available (e.g. after tab switching)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper.setData({
|
wrapper.setData({ posts: [], hasMore: false })
|
||||||
Post: null,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays no "load more" button', () => {
|
it('displays no "load more" button', () => {
|
||||||
@ -137,9 +135,7 @@ describe('ProfileSlug', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.setData({
|
wrapper.setData({ posts, hasMore: true })
|
||||||
Post: posts,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays a "load more" button', () => {
|
it('displays a "load more" button', () => {
|
||||||
@ -170,9 +166,7 @@ describe('ProfileSlug', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.setData({
|
wrapper.setData({ posts, hasMore: false })
|
||||||
Post: posts,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays no "load more" button', () => {
|
it('displays no "load more" button', () => {
|
||||||
|
|||||||
@ -72,7 +72,7 @@
|
|||||||
<template v-if="user.about">
|
<template v-if="user.about">
|
||||||
<hr />
|
<hr />
|
||||||
<ds-space margin-top="small" margin-bottom="small">
|
<ds-space margin-top="small" margin-bottom="small">
|
||||||
<ds-text color="soft" size="small">{{ user.about }}</ds-text>
|
<ds-text color="soft" size="small" class="hyphenate-text">{{ user.about }}</ds-text>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
</template>
|
</template>
|
||||||
</ds-card>
|
</ds-card>
|
||||||
@ -219,20 +219,20 @@
|
|||||||
</ds-space>
|
</ds-space>
|
||||||
</ds-grid-item>
|
</ds-grid-item>
|
||||||
|
|
||||||
<template v-if="activePosts.length">
|
<template v-if="posts.length">
|
||||||
<masonry-grid-item v-for="(post, index) in activePosts" :key="post.id">
|
<masonry-grid-item v-for="post in posts" :key="post.id">
|
||||||
<hc-post-card
|
<hc-post-card
|
||||||
:post="post"
|
:post="post"
|
||||||
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
||||||
@removePostFromList="removePostFromList(index)"
|
@removePostFromList="removePostFromList"
|
||||||
/>
|
/>
|
||||||
</masonry-grid-item>
|
</masonry-grid-item>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="$apollo.loading">
|
<template v-else-if="$apollo.loading">
|
||||||
<ds-grid-item>
|
<ds-grid-item column-span="fullWidth">
|
||||||
<ds-section centered>
|
<ds-space centered>
|
||||||
<ds-spinner size="base"></ds-spinner>
|
<ds-spinner size="base"></ds-spinner>
|
||||||
</ds-section>
|
</ds-space>
|
||||||
</ds-grid-item>
|
</ds-grid-item>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -306,33 +306,21 @@ export default {
|
|||||||
const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
|
const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
|
||||||
return {
|
return {
|
||||||
User: [],
|
User: [],
|
||||||
Post: [],
|
posts: [],
|
||||||
activePosts: [],
|
hasMore: false,
|
||||||
voted: false,
|
offset: 0,
|
||||||
page: 1,
|
|
||||||
pageSize: 6,
|
pageSize: 6,
|
||||||
tabActive: 'post',
|
tabActive: 'post',
|
||||||
filter,
|
filter,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasMore() {
|
|
||||||
const total = {
|
|
||||||
post: this.user.contributionsCount,
|
|
||||||
shout: this.user.shoutedCount,
|
|
||||||
comment: this.user.commentedCount,
|
|
||||||
}[this.tabActive]
|
|
||||||
return this.Post && this.Post.length < total
|
|
||||||
},
|
|
||||||
myProfile() {
|
myProfile() {
|
||||||
return this.$route.params.id === this.$store.getters['auth/user'].id
|
return this.$route.params.id === this.$store.getters['auth/user'].id
|
||||||
},
|
},
|
||||||
user() {
|
user() {
|
||||||
return this.User ? this.User[0] : {}
|
return this.User ? this.User[0] : {}
|
||||||
},
|
},
|
||||||
offset() {
|
|
||||||
return (this.page - 1) * this.pageSize
|
|
||||||
},
|
|
||||||
socialMediaLinks() {
|
socialMediaLinks() {
|
||||||
const { socialMedia = [] } = this.user
|
const { socialMedia = [] } = this.user
|
||||||
return socialMedia.map(socialMedia => {
|
return socialMedia.map(socialMedia => {
|
||||||
@ -355,19 +343,17 @@ export default {
|
|||||||
throw new Error('User not found!')
|
throw new Error('User not found!')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Post(val) {
|
|
||||||
this.activePosts = this.setActivePosts()
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removePostFromList(index) {
|
removePostFromList(deletedPost) {
|
||||||
this.activePosts.splice(index, 1)
|
this.posts = this.posts.filter(post => {
|
||||||
this.$apollo.queries.User.refetch()
|
return post.id !== deletedPost.id
|
||||||
|
})
|
||||||
},
|
},
|
||||||
handleTab(tab) {
|
handleTab(tab) {
|
||||||
this.tabActive = tab
|
this.tabActive = tab
|
||||||
this.Post = null
|
|
||||||
this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
|
this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
|
||||||
|
this.resetPostList()
|
||||||
},
|
},
|
||||||
uniq(items, field = 'id') {
|
uniq(items, field = 'id') {
|
||||||
return uniqBy(items, field)
|
return uniqBy(items, field)
|
||||||
@ -377,38 +363,23 @@ export default {
|
|||||||
this.$apollo.queries.User.refetch()
|
this.$apollo.queries.User.refetch()
|
||||||
},
|
},
|
||||||
showMoreContributions() {
|
showMoreContributions() {
|
||||||
// this.page++
|
this.offset += this.pageSize
|
||||||
// Fetch more data and transform the original result
|
|
||||||
this.page++
|
|
||||||
this.$apollo.queries.Post.fetchMore({
|
|
||||||
variables: {
|
|
||||||
filter: this.filter,
|
|
||||||
first: this.pageSize,
|
|
||||||
offset: this.offset,
|
|
||||||
},
|
|
||||||
// Transform the previous result with new data
|
|
||||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
|
||||||
let output = { Post: this.Post }
|
|
||||||
output.Post = [...previousResult.Post, ...fetchMoreResult.Post]
|
|
||||||
return output
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
setActivePosts() {
|
resetPostList() {
|
||||||
if (!this.Post) {
|
this.offset = 0
|
||||||
return []
|
this.posts = []
|
||||||
}
|
this.hasMore = false
|
||||||
return this.uniq(this.Post.filter(post => !post.deleted))
|
|
||||||
},
|
},
|
||||||
async block(user) {
|
async block(user) {
|
||||||
await this.$apollo.mutate({ mutation: Block(), variables: { id: user.id } })
|
await this.$apollo.mutate({ mutation: Block(), variables: { id: user.id } })
|
||||||
this.$apollo.queries.User.refetch()
|
this.$apollo.queries.User.refetch()
|
||||||
|
this.resetPostList()
|
||||||
this.$apollo.queries.Post.refetch()
|
this.$apollo.queries.Post.refetch()
|
||||||
},
|
},
|
||||||
async unblock(user) {
|
async unblock(user) {
|
||||||
await this.$apollo.mutate({ mutation: Unblock(), variables: { id: user.id } })
|
await this.$apollo.mutate({ mutation: Unblock(), variables: { id: user.id } })
|
||||||
this.$apollo.queries.User.refetch()
|
this.$apollo.queries.User.refetch()
|
||||||
|
this.resetPostList()
|
||||||
this.$apollo.queries.Post.refetch()
|
this.$apollo.queries.Post.refetch()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -421,10 +392,19 @@ export default {
|
|||||||
return {
|
return {
|
||||||
filter: this.filter,
|
filter: this.filter,
|
||||||
first: this.pageSize,
|
first: this.pageSize,
|
||||||
offset: 0,
|
offset: this.offset,
|
||||||
|
orderBy: 'createdAt_desc',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
|
update({ Post }) {
|
||||||
|
this.hasMore = Post && Post.length >= this.pageSize
|
||||||
|
if (!Post) return
|
||||||
|
// TODO: find out why `update` gets called twice initially.
|
||||||
|
// We have to filter for uniq posts only because we get the same
|
||||||
|
// result set twice.
|
||||||
|
this.posts = this.uniq([...this.posts, ...Post])
|
||||||
|
},
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
query() {
|
query() {
|
||||||
|
|||||||
@ -1,3 +1,10 @@
|
|||||||
|
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'
|
||||||
|
import introspectionQueryResultData from './apollo-config/fragmentTypes.json'
|
||||||
|
|
||||||
|
const fragmentMatcher = new IntrospectionFragmentMatcher({
|
||||||
|
introspectionQueryResultData,
|
||||||
|
})
|
||||||
|
|
||||||
export default ({ app }) => {
|
export default ({ app }) => {
|
||||||
const backendUrl = process.env.GRAPHQL_URI || 'http://localhost:4000'
|
const backendUrl = process.env.GRAPHQL_URI || 'http://localhost:4000'
|
||||||
|
|
||||||
@ -10,5 +17,6 @@ export default ({ app }) => {
|
|||||||
tokenName: 'human-connection-token',
|
tokenName: 'human-connection-token',
|
||||||
persisting: false,
|
persisting: false,
|
||||||
websocketsOnly: false,
|
websocketsOnly: false,
|
||||||
|
cache: new InMemoryCache({ fragmentMatcher }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
webapp/plugins/apollo-config/fragmentTypes.json
Normal file
18
webapp/plugins/apollo-config/fragmentTypes.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"__schema": {
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"kind": "UNION",
|
||||||
|
"name": "NotificationSource",
|
||||||
|
"possibleTypes": [
|
||||||
|
{
|
||||||
|
"name": "Post"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comment"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -86,23 +86,6 @@ export const actions = {
|
|||||||
id
|
id
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
notifications(read: false, orderBy: createdAt_desc) {
|
|
||||||
id
|
|
||||||
read
|
|
||||||
createdAt
|
|
||||||
post {
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
name
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
}
|
|
||||||
title
|
|
||||||
contentExcerpt
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@ -1,99 +0,0 @@
|
|||||||
import gql from 'graphql-tag'
|
|
||||||
|
|
||||||
export const state = () => {
|
|
||||||
return {
|
|
||||||
notifications: null,
|
|
||||||
pending: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mutations = {
|
|
||||||
SET_NOTIFICATIONS(state, notifications) {
|
|
||||||
state.notifications = notifications
|
|
||||||
},
|
|
||||||
SET_PENDING(state, pending) {
|
|
||||||
state.pending = pending
|
|
||||||
},
|
|
||||||
UPDATE_NOTIFICATIONS(state, notification) {
|
|
||||||
const notifications = state.notifications
|
|
||||||
const toBeUpdated = notifications.find(n => {
|
|
||||||
return n.id === notification.id
|
|
||||||
})
|
|
||||||
state.notifications = {
|
|
||||||
...toBeUpdated,
|
|
||||||
...notification,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
export const getters = {
|
|
||||||
notifications(state) {
|
|
||||||
return !!state.notifications
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
async init({ getters, commit }) {
|
|
||||||
if (getters.notifications) return
|
|
||||||
commit('SET_PENDING', true)
|
|
||||||
const client = this.app.apolloProvider.defaultClient
|
|
||||||
let notifications
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
data: { currentUser },
|
|
||||||
} = await client.query({
|
|
||||||
query: gql`
|
|
||||||
{
|
|
||||||
currentUser {
|
|
||||||
id
|
|
||||||
notifications(orderBy: createdAt_desc) {
|
|
||||||
id
|
|
||||||
read
|
|
||||||
createdAt
|
|
||||||
post {
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
name
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
}
|
|
||||||
title
|
|
||||||
contentExcerpt
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
notifications = currentUser.notifications
|
|
||||||
commit('SET_NOTIFICATIONS', notifications)
|
|
||||||
} finally {
|
|
||||||
commit('SET_PENDING', false)
|
|
||||||
}
|
|
||||||
return notifications
|
|
||||||
},
|
|
||||||
|
|
||||||
async markAsRead({ commit, rootGetters }, notificationId) {
|
|
||||||
const client = this.app.apolloProvider.defaultClient
|
|
||||||
const mutation = gql`
|
|
||||||
mutation($id: ID!, $read: Boolean!) {
|
|
||||||
UpdateNotification(id: $id, read: $read) {
|
|
||||||
id
|
|
||||||
read
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const variables = {
|
|
||||||
id: notificationId,
|
|
||||||
read: true,
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
data: { UpdateNotification },
|
|
||||||
} = await client.mutate({
|
|
||||||
mutation,
|
|
||||||
variables,
|
|
||||||
})
|
|
||||||
commit('UPDATE_NOTIFICATIONS', UpdateNotification)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@ -46,8 +46,8 @@ export const actions = {
|
|||||||
await this.app.apolloProvider.defaultClient
|
await this.app.apolloProvider.defaultClient
|
||||||
.query({
|
.query({
|
||||||
query: gql`
|
query: gql`
|
||||||
query findPosts($query: String!) {
|
query findPosts($query: String!, $filter: _PostFilter) {
|
||||||
findPosts(query: $query, limit: 10) {
|
findPosts(query: $query, limit: 10, filter: $filter) {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
label: title
|
label: title
|
||||||
@ -64,6 +64,7 @@ export const actions = {
|
|||||||
`,
|
`,
|
||||||
variables: {
|
variables: {
|
||||||
query: value.replace(/\s/g, '~ ') + '~',
|
query: value.replace(/\s/g, '~ ') + '~',
|
||||||
|
filter: {},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
|||||||
@ -5379,13 +5379,12 @@ create-react-context@^0.2.1:
|
|||||||
fbjs "^0.8.0"
|
fbjs "^0.8.0"
|
||||||
gud "^1.0.0"
|
gud "^1.0.0"
|
||||||
|
|
||||||
cross-env@~5.2.0:
|
cross-env@~5.2.1:
|
||||||
version "5.2.0"
|
version "5.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2"
|
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.1.tgz#b2c76c1ca7add66dc874d11798466094f551b34d"
|
||||||
integrity sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==
|
integrity sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn "^6.0.5"
|
cross-spawn "^6.0.5"
|
||||||
is-windows "^1.0.0"
|
|
||||||
|
|
||||||
cross-fetch@^3.0.4:
|
cross-fetch@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
@ -6387,12 +6386,12 @@ eslint-module-utils@^2.4.0:
|
|||||||
debug "^2.6.8"
|
debug "^2.6.8"
|
||||||
pkg-dir "^2.0.0"
|
pkg-dir "^2.0.0"
|
||||||
|
|
||||||
eslint-plugin-es@^1.4.0:
|
eslint-plugin-es@^1.4.1:
|
||||||
version "1.4.0"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz#12acae0f4953e76ba444bfd1b2271081ac620998"
|
||||||
integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw==
|
integrity sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint-utils "^1.3.0"
|
eslint-utils "^1.4.2"
|
||||||
regexpp "^2.0.1"
|
regexpp "^2.0.1"
|
||||||
|
|
||||||
eslint-plugin-import@~2.18.2:
|
eslint-plugin-import@~2.18.2:
|
||||||
@ -6412,20 +6411,20 @@ eslint-plugin-import@~2.18.2:
|
|||||||
read-pkg-up "^2.0.0"
|
read-pkg-up "^2.0.0"
|
||||||
resolve "^1.11.0"
|
resolve "^1.11.0"
|
||||||
|
|
||||||
eslint-plugin-jest@~22.15.2:
|
eslint-plugin-jest@~22.16.0:
|
||||||
version "22.15.2"
|
version "22.16.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.2.tgz#e3c10d9391f787744e31566f69ebb70c3a98e398"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.16.0.tgz#30c4e0e9dc331beb2e7369b70dd1363690c1ce05"
|
||||||
integrity sha512-p4NME9TgXIt+KgpxcXyNBvO30ZKxwFAO1dJZBc2OGfDnXVEtPwEyNs95GSr6RIE3xLHdjd8ngDdE2icRRXrbxg==
|
integrity sha512-eBtSCDhO1k7g3sULX/fuRK+upFQ7s548rrBtxDyM1fSoY7dTWp/wICjrJcDZKVsW7tsFfH22SG+ZaxG5BZodIg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/experimental-utils" "^1.13.0"
|
"@typescript-eslint/experimental-utils" "^1.13.0"
|
||||||
|
|
||||||
eslint-plugin-node@~9.1.0:
|
eslint-plugin-node@~9.2.0:
|
||||||
version "9.1.0"
|
version "9.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.2.0.tgz#b1911f111002d366c5954a6d96d3cd5bf2a3036a"
|
||||||
integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw==
|
integrity sha512-2abNmzAH/JpxI4gEOwd6K8wZIodK3BmHbTxz4s79OIYwwIt2gkpEXlAouJXu4H1c9ySTnRso0tsuthSOZbUMlA==
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint-plugin-es "^1.4.0"
|
eslint-plugin-es "^1.4.1"
|
||||||
eslint-utils "^1.3.1"
|
eslint-utils "^1.4.2"
|
||||||
ignore "^5.1.1"
|
ignore "^5.1.1"
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
resolve "^1.10.1"
|
resolve "^1.10.1"
|
||||||
@ -6463,7 +6462,7 @@ eslint-scope@^4.0.0, eslint-scope@^4.0.3:
|
|||||||
esrecurse "^4.1.0"
|
esrecurse "^4.1.0"
|
||||||
estraverse "^4.1.1"
|
estraverse "^4.1.1"
|
||||||
|
|
||||||
eslint-utils@^1.3.0, eslint-utils@^1.3.1:
|
eslint-utils@^1.3.1, eslint-utils@^1.4.2:
|
||||||
version "1.4.2"
|
version "1.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
|
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
|
||||||
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
|
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
|
||||||
@ -7635,10 +7634,10 @@ graphql-upload@^8.0.2:
|
|||||||
http-errors "^1.7.2"
|
http-errors "^1.7.2"
|
||||||
object-path "^0.11.4"
|
object-path "^0.11.4"
|
||||||
|
|
||||||
"graphql@14.0.2 - 14.2.0 || ^14.3.1", graphql@^14.4.0, graphql@~14.5.3:
|
"graphql@14.0.2 - 14.2.0 || ^14.3.1", graphql@^14.4.0, graphql@~14.5.4:
|
||||||
version "14.5.3"
|
version "14.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.3.tgz#e025851cc413e153220f4edbbb25d49f55104fa0"
|
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.4.tgz#b33fe957854e90c10d4c07c7d26b6c8e9f159a13"
|
||||||
integrity sha512-W8A8nt9BsMg0ZK2qA3DJIVU6muWhxZRYLTmc+5XGwzWzVdUdPVlAAg5hTBjiTISEnzsKL/onasu6vl3kgGTbYg==
|
integrity sha512-dPLvHoxy5m9FrkqWczPPRnH0X80CyvRE6e7Fa5AWEqEAzg9LpxHvKh24po/482E6VWHigOkAmb4xCp6P9yT9gw==
|
||||||
dependencies:
|
dependencies:
|
||||||
iterall "^1.2.2"
|
iterall "^1.2.2"
|
||||||
|
|
||||||
@ -8162,12 +8161,7 @@ ignore@^4.0.6:
|
|||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||||
|
|
||||||
ignore@^5.1.1:
|
ignore@^5.1.1, ignore@^5.1.4:
|
||||||
version "5.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.1.tgz#2fc6b8f518aff48fef65a7f348ed85632448e4a5"
|
|
||||||
integrity sha512-DWjnQIFLenVrwyRCKZT+7a7/U4Cqgar4WG8V++K3hw+lrW1hc/SIwdiGmtxKCVACmHULTuGeBbHJmbwW7/sAvA==
|
|
||||||
|
|
||||||
ignore@^5.1.4:
|
|
||||||
version "5.1.4"
|
version "5.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
|
||||||
integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
|
integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
|
||||||
@ -8752,7 +8746,7 @@ is-window@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d"
|
resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d"
|
||||||
integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0=
|
integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0=
|
||||||
|
|
||||||
is-windows@^1.0.0, is-windows@^1.0.2:
|
is-windows@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||||
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
|
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
|
||||||
@ -13345,7 +13339,7 @@ sass-graph@^2.2.4:
|
|||||||
scss-tokenizer "^0.2.3"
|
scss-tokenizer "^0.2.3"
|
||||||
yargs "^7.0.0"
|
yargs "^7.0.0"
|
||||||
|
|
||||||
sass-loader@^7.1.0, sass-loader@~7.3.1:
|
sass-loader@^7.1.0:
|
||||||
version "7.3.1"
|
version "7.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.3.1.tgz#a5bf68a04bcea1c13ff842d747150f7ab7d0d23f"
|
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.3.1.tgz#a5bf68a04bcea1c13ff842d747150f7ab7d0d23f"
|
||||||
integrity sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==
|
integrity sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==
|
||||||
@ -13356,6 +13350,17 @@ sass-loader@^7.1.0, sass-loader@~7.3.1:
|
|||||||
pify "^4.0.1"
|
pify "^4.0.1"
|
||||||
semver "^6.3.0"
|
semver "^6.3.0"
|
||||||
|
|
||||||
|
sass-loader@~8.0.0:
|
||||||
|
version "8.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.0.tgz#e7b07a3e357f965e6b03dd45b016b0a9746af797"
|
||||||
|
integrity sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==
|
||||||
|
dependencies:
|
||||||
|
clone-deep "^4.0.1"
|
||||||
|
loader-utils "^1.2.3"
|
||||||
|
neo-async "^2.6.1"
|
||||||
|
schema-utils "^2.1.0"
|
||||||
|
semver "^6.3.0"
|
||||||
|
|
||||||
sass-resources-loader@^2.0.0:
|
sass-resources-loader@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-2.0.0.tgz#88569c542fbf1f18f33a6578b77cc5b36c56911d"
|
resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-2.0.0.tgz#88569c542fbf1f18f33a6578b77cc5b36c56911d"
|
||||||
@ -15173,10 +15178,10 @@ vue-infinite-scroll@^2.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/vue-infinite-scroll/-/vue-infinite-scroll-2.0.2.tgz#ca37a91fe92ee0ad3b74acf8682c00917144b711"
|
resolved "https://registry.yarnpkg.com/vue-infinite-scroll/-/vue-infinite-scroll-2.0.2.tgz#ca37a91fe92ee0ad3b74acf8682c00917144b711"
|
||||||
integrity sha512-n+YghR059YmciANGJh9SsNWRi1YZEBVlODtmnb/12zI+4R72QZSWd+EuZ5mW6auEo/yaJXgxzwsuhvALVnm73A==
|
integrity sha512-n+YghR059YmciANGJh9SsNWRi1YZEBVlODtmnb/12zI+4R72QZSWd+EuZ5mW6auEo/yaJXgxzwsuhvALVnm73A==
|
||||||
|
|
||||||
vue-izitoast@^1.2.0:
|
vue-izitoast@^1.2.1:
|
||||||
version "1.2.0"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/vue-izitoast/-/vue-izitoast-1.2.0.tgz#55b7434a391c6eb64dd10c0de211e99ba7e486e2"
|
resolved "https://registry.yarnpkg.com/vue-izitoast/-/vue-izitoast-1.2.1.tgz#cd2cbfbd96ea438dede8fb00f2c328364cb7141d"
|
||||||
integrity sha512-Jqxfid12SUBIySJxgyPpu6gZ1ssMcbKtCvu9uMQPNM8RUnd3RKC4nyxkncdYe5L6XPU+SaznjYRudnvtclY4wA==
|
integrity sha512-5krrKyAftSR3TnnO3zhMihYCSt0Lay4SBO1AWWKD3jhTErJrR+q9kOKyuAYhn1SttNER87hpnRKqdvLjzjHWQQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
izitoast "^1.4.0"
|
izitoast "^1.4.0"
|
||||||
|
|
||||||
|
|||||||
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