Merge branch 'master' of github.com:Human-Connection/Human-Connection into 1395-hashtags-imported-with-not-allowed-chars

This commit is contained in:
Matt Rider 2019-09-02 12:51:11 +02:00
commit d1309d2b8f
48 changed files with 987 additions and 973 deletions

View File

@ -49,7 +49,7 @@
"apollo-client": "~2.6.4",
"apollo-link-context": "~1.0.18",
"apollo-link-http": "~1.5.15",
"apollo-server": "~2.9.1",
"apollo-server": "~2.9.3",
"apollo-server-express": "^2.9.0",
"babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3",
@ -61,7 +61,7 @@
"dotenv": "~8.1.0",
"express": "^4.17.1",
"faker": "Marak/faker.js#master",
"graphql": "^14.5.3",
"graphql": "^14.5.4",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.5",
@ -90,7 +90,7 @@
"metascraper-video": "^5.6.5",
"metascraper-youtube": "^5.6.3",
"minimatch": "^3.0.4",
"neo4j-driver": "~1.7.5",
"neo4j-driver": "~1.7.6",
"neo4j-graphql-js": "^2.7.2",
"neode": "^0.3.2",
"node-fetch": "~2.6.0",
@ -117,12 +117,12 @@
"babel-jest": "~24.9.0",
"chai": "~4.2.0",
"cucumber": "~5.1.0",
"eslint": "~6.2.2",
"eslint": "~6.3.0",
"eslint-config-prettier": "~6.1.0",
"eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.15.2",
"eslint-plugin-node": "~9.1.0",
"eslint-plugin-jest": "~22.16.0",
"eslint-plugin-node": "~9.2.0",
"eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1",

View File

@ -12,11 +12,9 @@ export default {
CreatePost: setCreatedAt,
CreateComment: setCreatedAt,
CreateOrganization: setCreatedAt,
CreateNotification: setCreatedAt,
UpdateUser: setUpdatedAt,
UpdatePost: setUpdatedAt,
UpdateComment: setUpdatedAt,
UpdateOrganization: setUpdatedAt,
UpdateNotification: setUpdatedAt,
},
}

View File

@ -4,13 +4,13 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
if (!idsOfUsers.length) return
// 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)) {
throw new Error('Notification reason is not allowed!')
}
if (
(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!')
}
@ -25,8 +25,9 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
MATCH (user: User)
WHERE user.id in $idsOfUsers
AND NOT (user)<-[:BLOCKED]-(author)
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt })
MERGE (post)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
SET notification.read = FALSE
SET notification.createdAt = $createdAt
`
break
}
@ -37,20 +38,22 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
WHERE user.id in $idsOfUsers
AND NOT (user)<-[:BLOCKED]-(author)
AND NOT (user)<-[:BLOCKED]-(postAuthor)
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt })
MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
SET notification.read = FALSE
SET notification.createdAt = $createdAt
`
break
}
case 'comment_on_post': {
case 'commented_on_post': {
cypher = `
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
MATCH (user: User)
WHERE user.id in $idsOfUsers
AND NOT (user)<-[:BLOCKED]-(author)
AND NOT (author)<-[:BLOCKED]-(user)
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt })
MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
SET notification.read = FALSE
SET notification.createdAt = $createdAt
`
break
}
@ -105,7 +108,7 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) =>
return record.get('user')
})
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)
}
}

View File

@ -77,14 +77,18 @@ afterEach(async () => {
describe('notifications', () => {
const notificationQuery = gql`
query($read: Boolean) {
currentUser {
notifications(read: $read, orderBy: createdAt_desc) {
read
reason
post {
notifications(read: $read, orderBy: createdAt_desc) {
read
reason
createdAt
from {
__typename
... on Post {
id
content
}
comment {
... on Comment {
id
content
}
}
@ -154,18 +158,18 @@ describe('notifications', () => {
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [
{
read: false,
reason: 'comment_on_post',
post: null,
comment: {
content: commentContent,
},
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'commented_on_post',
from: {
__typename: 'Comment',
id: 'c47',
content: commentContent,
},
],
},
},
],
},
})
const { query } = createTestClient(server)
@ -183,11 +187,7 @@ describe('notifications', () => {
await notifiedUser.relateTo(commentAuthor, 'blocked')
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [],
},
},
data: { notifications: [] },
})
const { query } = createTestClient(server)
await expect(
@ -211,11 +211,7 @@ describe('notifications', () => {
await notifiedUser.relateTo(commentAuthor, 'blocked')
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [],
},
},
data: { notifications: [] },
})
const { query } = createTestClient(server)
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?'
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [
{
read: false,
reason: 'mentioned_in_post',
post: {
content: expectedContent,
},
comment: null,
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'mentioned_in_post',
from: {
__typename: 'Post',
id: 'p47',
content: expectedContent,
},
],
},
},
],
},
})
const { query } = createTestClient(server)
@ -278,7 +274,7 @@ describe('notifications', () => {
).resolves.toEqual(expected)
})
describe('many times', () => {
describe('updates the post and mentions me again', () => {
const updatePostAction = async () => {
const updatedContent = `
One more mention to
@ -307,33 +303,25 @@ describe('notifications', () => {
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 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({
data: {
currentUser: {
notifications: [
{
read: false,
reason: 'mentioned_in_post',
post: {
content: expectedContent,
},
comment: null,
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'mentioned_in_post',
from: {
__typename: 'Post',
id: 'p47',
content: expectedUpdatedContent,
},
{
read: false,
reason: 'mentioned_in_post',
post: {
content: expectedContent,
},
comment: null,
},
],
},
},
],
},
})
await expect(
@ -345,6 +333,68 @@ describe('notifications', () => {
}),
).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', () => {
@ -355,11 +405,7 @@ describe('notifications', () => {
it('sends no notification', async () => {
await createPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [],
},
},
data: { notifications: [] },
})
const { query } = createTestClient(server)
await expect(
@ -397,18 +443,18 @@ describe('notifications', () => {
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [
{
read: false,
reason: 'mentioned_in_comment',
post: null,
comment: {
content: commentContent,
},
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'mentioned_in_comment',
from: {
__typename: 'Comment',
id: 'c47',
content: commentContent,
},
],
},
},
],
},
})
const { query } = createTestClient(server)
@ -440,11 +486,7 @@ describe('notifications', () => {
it('sends no notification', async () => {
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [],
},
},
data: { notifications: [] },
})
const { query } = createTestClient(server)
await expect(

View File

@ -41,32 +41,6 @@ const isMySocialMedia = rule({
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
* `onlyEnabledContent` throws authorization errors only if you have
* arguments for `disabled` or `deleted` assuming these are filter
@ -149,7 +123,6 @@ const permissions = shield(
Category: allow,
Tag: allow,
Report: isModerator,
Notification: isAdmin,
statistics: allow,
currentUser: allow,
Post: or(onlyEnabledContent, isModerator),
@ -160,6 +133,7 @@ const permissions = shield(
PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: allow,
blockedUsers: isAuthenticated,
notifications: isAuthenticated,
},
Mutation: {
'*': deny,
@ -168,7 +142,6 @@ const permissions = shield(
Signup: isAdmin,
SignupVerification: allow,
CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)),
UpdateNotification: belongsToMe,
UpdateUser: onlyYourself,
CreatePost: isAuthenticated,
UpdatePost: isAuthor,
@ -198,6 +171,7 @@ const permissions = shield(
RemovePostEmotions: isAuthenticated,
block: isAuthenticated,
unblock: isAuthenticated,
markAsRead: isAuthenticated,
},
User: {
email: isMyOwn,

View File

@ -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',
},
}

View File

@ -7,6 +7,5 @@ export default {
EmailAddress: require('./EmailAddress.js'),
SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'),
Notification: require('./Notification.js'),
Category: require('./Category.js'),
}

View File

@ -20,6 +20,7 @@ export default applyScalars(
'Statistics',
'LoggedInUser',
'SocialMedia',
'NOTIFIED',
],
// add 'User' here as soon as possible
},
@ -32,6 +33,7 @@ export default applyScalars(
'Statistics',
'LoggedInUser',
'SocialMedia',
'NOTIFIED',
],
// add 'User' here as soon as possible
},

View File

@ -1,4 +1,5 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
import Resolver from './helpers/Resolver'
export default {
Mutation: {
@ -52,4 +53,13 @@ export default {
return comment
},
},
Comment: {
...Resolver('Comment', {
hasOne: {
author: '<-[:WROTE]-(related:User)',
post: '-[:COMMENTS]->(related:Post)',
disabledBy: '<-[:DISABLED]-(related:User)',
},
}),
},
}

View File

@ -61,7 +61,6 @@ export default function Resolver(type, options = {}) {
const id = parent[idAttribute]
const statement = `
MATCH(u:${type} {${idAttribute}: {id}})${connection}
WHERE NOT related.deleted = true AND NOT related.disabled = true
RETURN COUNT(DISTINCT(related)) as count
`
const result = await instance.cypher(statement, { id })

View File

@ -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 {
Query: {
Notification: (object, params, context, resolveInfo) => {
return neo4jgraphql(object, params, context, resolveInfo, false)
notifications: async (parent, args, context, resolveInfo) => {
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)-[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: {
UpdateNotification: (object, params, context, resolveInfo) => {
return neo4jgraphql(object, params, context, resolveInfo, false)
markAsRead: async (parent, args, context, resolveInfo) => {
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
},
},
}

View File

@ -1,397 +1,309 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories'
import { host, login, gql } from '../../jest/helpers'
import { neode } from '../../bootstrap/neo4j'
import { gql } from '../../jest/helpers'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'
let client
const factory = Factory()
const instance = neode()
const neode = getNeode()
const driver = getDriver()
const userParams = {
id: 'you',
email: 'test@example.org',
password: '1234',
}
const categoryIds = ['cat9']
let authenticatedUser
let user
let variables
let query
let mutate
beforeAll(() => {
const { server } = createServer({
context: () => {
return {
driver,
user: authenticatedUser,
}
},
})
query = createTestClient(server).query
mutate = createTestClient(server).mutate
})
beforeEach(async () => {
await factory.create('User', userParams)
await instance.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
})
authenticatedUser = null
variables = { orderBy: 'createdAt_asc' }
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('Notification', () => {
const notificationQuery = gql`
query {
Notification {
id
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(notificationQuery)).rejects.toThrow('Not Authorised')
})
describe('given some notifications', () => {
beforeEach(async () => {
user = await factory.create('User', userParams)
await factory.create('User', { id: 'neighbor' })
await Promise.all(setupNotifications.map(s => neode.cypher(s)))
})
})
const setupNotifications = [
`MATCH(user:User {id: 'neighbor'})
MERGE (:Post {id: 'p1', content: 'Not for you'})
-[:NOTIFIED {createdAt: "2019-08-29T17:33:48.651Z", read: false, reason: "mentioned_in_post"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MERGE (:Post {id: 'p2', content: 'Already seen post mentioning'})
-[:NOTIFIED {createdAt: "2019-08-30T17:33:48.651Z", read: true, reason: "mentioned_in_post"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MERGE (:Post {id: 'p3', content: 'You have been mentioned in a post'})
-[:NOTIFIED {createdAt: "2019-08-31T17:33:48.651Z", read: false, reason: "mentioned_in_post"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MATCH(post:Post {id: 'p3'})
CREATE (comment:Comment {id: 'c1', content: 'You have seen this comment mentioning already'})
MERGE (comment)-[:COMMENTS]->(post)
MERGE (comment)
-[:NOTIFIED {createdAt: "2019-08-30T15:33:48.651Z", read: true, reason: "mentioned_in_comment"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MATCH(post:Post {id: 'p3'})
CREATE (comment:Comment {id: 'c2', content: 'You have been mentioned in a comment'})
MERGE (comment)-[:COMMENTS]->(post)
MERGE (comment)
-[:NOTIFIED {createdAt: "2019-08-30T19:33:48.651Z", read: false, reason: "mentioned_in_comment"}]
->(user);
`,
`MATCH(user:User {id: 'neighbor'})
MATCH(post:Post {id: 'p3'})
CREATE (comment:Comment {id: 'c3', content: 'Somebody else was mentioned in a comment'})
MERGE (comment)-[:COMMENTS]->(post)
MERGE (comment)
-[:NOTIFIED {createdAt: "2019-09-01T17:33:48.651Z", read: false, reason: "mentioned_in_comment"}]
->(user);
`,
]
describe('currentUser notifications', () => {
const variables = {}
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
})
describe('given some notifications', () => {
beforeEach(async () => {
const neighborParams = {
email: 'neighbor@example.org',
password: '1234',
id: 'neighbor',
describe('notifications', () => {
const notificationQuery = gql`
query($read: Boolean, $orderBy: NotificationOrdering) {
notifications(read: $read, orderBy: $orderBy) {
from {
__typename
... on Post {
content
}
... on Comment {
content
}
}
read
createdAt
}
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', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
).rejects.toThrow('Not Authorised')
const result = await query({ query: notificationQuery })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('authenticated', () => {
beforeEach(async () => {
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
authenticatedUser = await user.toJson()
})
describe('no filters', () => {
it('returns all notifications of current user', async () => {
const expected = expect.objectContaining({
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 mentioning',
},
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.toEqual(expected)
})
})
it('throws authorization error', async () => {
await expect(
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
).rejects.toThrow('Not Authorised')
})
describe('and owner', () => {
beforeEach(async () => {
headers = await login({
email: 'mentioned@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
})
it('updates post notification', async () => {
const expected = {
UpdateNotification: {
id: 'post-mention-to-be-updated',
read: true,
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(
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
).resolves.toEqual(expected)
})
it('updates comment notification', async () => {
const expected = {
UpdateNotification: {
id: 'comment-mention-to-be-updated',
read: true,
},
}
await expect(
client.request(mutationUpdateNotification, variablesCommentUpdateNotification),
query({ query: notificationQuery, variables: { ...variables, read: false } }),
).resolves.toEqual(expected)
})
})
})
})
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 () => {
const result = await mutate({
mutation: markAsReadMutation,
variables: { ...variables, id: 'p1' },
})
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('authenticated', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
})
describe('not being notified at all', () => {
beforeEach(async () => {
variables = {
...variables,
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',
}
})
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()
})
})
})
describe('on a comment', () => {
beforeEach(async () => {
variables = {
...variables,
id: 'c2',
}
})
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
const { data } = await mutate({ mutation: markAsReadMutation, variables })
expect(data).toEqual({
markAsRead: {
from: {
__typename: 'Comment',
content: 'You have been mentioned in a comment',
},
read: true,
createdAt: '2019-08-30T19:33:48.651Z',
},
})
})
})
})
})
})
})

View File

@ -3,6 +3,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload'
import { getBlockedUsers, getBlockedByUsers } from './users.js'
import { mergeWith, isArray } from 'lodash'
import Resolver from './helpers/Resolver'
const filterForBlockedUsers = async (params, context) => {
if (!context.user) return params
@ -11,6 +12,8 @@ const filterForBlockedUsers = async (params, context) => {
getBlockedByUsers(context),
])
const badIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)]
if (!badIds.length) return params
params.filter = mergeWith(
params.filter,
{
@ -179,4 +182,46 @@ export default {
return emoted
},
},
Post: {
...Resolver('Post', {
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: {
shoutedCount:
'<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true',
emotionsCount: '<-[related:EMOTED]-(:User)',
},
boolean: {
shoutedByCurrentUser:
'<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 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)
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
},
},
}

View File

@ -147,12 +147,15 @@ export default {
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
},
count: {
contributionsCount: '-[:WROTE]->(related:Post)',
contributionsCount:
'-[:WROTE]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
friendsCount: '<-[:FRIENDS]->(related:User)',
followingCount: '-[:FOLLOWS]->(related:User)',
followedByCount: '<-[:FOLLOWS]-(related:User)',
commentedCount: '-[:WROTE]->(:Comment)-[:COMMENTS]->(related:Post)',
shoutedCount: '-[:SHOUTED]->(related:Post)',
commentedCount:
'-[: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)',
},
hasOne: {

View File

@ -1,5 +1,5 @@
enum ReasonNotification {
mentioned_in_post
mentioned_in_comment
comment_on_post
}
commented_on_post
}

View File

@ -4,7 +4,7 @@ type Query {
currentUser: User
# Get the latest Network Statistics
statistics: Statistics!
findPosts(query: String!, limit: Int = 10): [Post]!
findPosts(query: String!, limit: Int = 10, filter: _PostFilter): [Post]!
@cypher(
statement: """
CALL db.index.fulltext.queryNodes('full_text_search', $query)

View 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
}

View File

@ -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")
}

View File

@ -24,8 +24,6 @@ type User {
createdAt: String
updatedAt: String
notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
@ -64,7 +62,7 @@ type User {
)
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")
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")

View File

@ -8,7 +8,6 @@ import createComment from './comments.js'
import createCategory from './categories.js'
import createTag from './tags.js'
import createReport from './reports.js'
import createNotification from './notifications.js'
export const seedServerHost = 'http://127.0.0.1:4001'
@ -31,7 +30,6 @@ const factories = {
Category: createCategory,
Tag: createTag,
Report: createReport,
Notification: createNotification,
}
export const cleanDatabase = async (options = {}) => {

View File

@ -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 },
}
}

View File

@ -270,7 +270,7 @@ import Factory from './factories'
const hashtag1 =
'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!'
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. ;-)'
await Promise.all([
asAdmin.create('Post', {

View File

@ -689,7 +689,7 @@
core-js "^2.6.5"
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"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
@ -1546,6 +1546,14 @@ apollo-cache-control@0.8.2:
apollo-server-env "2.4.2"
graphql-extensions "0.10.1"
apollo-cache-control@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.4.tgz#a3650d5e4173953e2a3af995bea62147f1ffe4d7"
integrity sha512-IZ1d3AXZtkZhLYo0kWqTbZ6nqLFaeUvLdMESs+9orMadBZ7mvzcAfBwrhKyCWPGeAAZ/jKv8FtYHybpchHgFAg==
dependencies:
apollo-server-env "^2.4.3"
graphql-extensions "^0.10.3"
apollo-cache-inmemory@~1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d"
@ -1587,7 +1595,15 @@ apollo-datasource@0.6.2:
apollo-server-caching "0.5.0"
apollo-server-env "2.4.2"
apollo-engine-reporting-protobuf@0.4.0:
apollo-datasource@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.3.tgz#b31e089e52adb92fabb536ab8501c502573ffe13"
integrity sha512-gRYyFVpJgHE2hhS+VxMeOerxXQ/QYxWG7T6QddfugJWYAG9DRCl65e2b7txcGq2NP3r+O1iCm4GNwhRBDJbd8A==
dependencies:
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-engine-reporting-protobuf@0.4.0, apollo-engine-reporting-protobuf@^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"
integrity sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA==
@ -1607,6 +1623,19 @@ apollo-engine-reporting@1.4.4:
async-retry "^1.2.1"
graphql-extensions "0.10.1"
apollo-engine-reporting@^1.4.6:
version "1.4.6"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.6.tgz#83af6689c4ab82d1c62c3f5dde7651975508114f"
integrity sha512-acfb7oFnru/8YQdY4x6+7WJbZfzdVETI8Cl+9ImgUrvUnE8P+f2SsGTKXTC1RuUvve4c56PAvaPgE+z8X1a1Mw==
dependencies:
apollo-engine-reporting-protobuf "^0.4.0"
apollo-graphql "^0.3.3"
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server-types "^0.2.4"
async-retry "^1.2.1"
graphql-extensions "^0.10.3"
apollo-env@0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.5.1.tgz#b9b0195c16feadf0fe9fd5563edb0b9b7d9e97d3"
@ -1668,7 +1697,7 @@ apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.3:
tslib "^1.9.3"
zen-observable-ts "^0.8.19"
apollo-server-caching@0.5.0:
apollo-server-caching@0.5.0, apollo-server-caching@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46"
integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw==
@ -1702,6 +1731,33 @@ apollo-server-core@2.9.1:
subscriptions-transport-ws "^0.9.11"
ws "^6.0.0"
apollo-server-core@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.3.tgz#918f836c8215d371935c831c72d0840c7bf0250f"
integrity sha512-KQpOM3nAXdMqKVE0HHcOkH/EVhyDqFEKLNFlsyGHGOn9ujpI6RsltX+YpXRyAdbfQHpTk11v/IAo6XksWN+g1Q==
dependencies:
"@apollographql/apollo-tools" "^0.4.0"
"@apollographql/graphql-playground-html" "1.6.24"
"@types/graphql-upload" "^8.0.0"
"@types/ws" "^6.0.0"
apollo-cache-control "^0.8.4"
apollo-datasource "^0.6.3"
apollo-engine-reporting "^1.4.6"
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server-errors "^2.3.3"
apollo-server-plugin-base "^0.6.4"
apollo-server-types "^0.2.4"
apollo-tracing "^0.8.4"
fast-json-stable-stringify "^2.0.0"
graphql-extensions "^0.10.3"
graphql-tag "^2.9.2"
graphql-tools "^4.0.0"
graphql-upload "^8.0.2"
sha.js "^2.4.11"
subscriptions-transport-ws "^0.9.11"
ws "^6.0.0"
apollo-server-env@2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.2.tgz#8549caa7c8f57af88aadad5c2a0bb7adbcc5f76e"
@ -1710,15 +1766,28 @@ apollo-server-env@2.4.2:
node-fetch "^2.1.2"
util.promisify "^1.0.0"
apollo-server-env@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.3.tgz#9bceedaae07eafb96becdfd478f8d92617d825d2"
integrity sha512-23R5Xo9OMYX0iyTu2/qT0EUb+AULCBriA9w8HDfMoChB8M+lFClqUkYtaTTHDfp6eoARLW8kDBhPOBavsvKAjA==
dependencies:
node-fetch "^2.1.2"
util.promisify "^1.0.0"
apollo-server-errors@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.2.tgz#86bbd1ff8f0b5f16bfdcbb1760398928f9fce539"
integrity sha512-twVCP8tNHFzxOzU3jf84ppBFSvjvisZVWlgF82vwG+qEEUaAE5h5DVpeJbcI1vRW4VQPuFV+B+FIsnlweFKqtQ==
apollo-server-express@2.9.1, apollo-server-express@^2.9.0:
version "2.9.1"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.1.tgz#9a8cb7fba579e68ddfa1953dfd066b751bca32f0"
integrity sha512-3mmuojt9s9Gyqdf8fbdKtbw23UFYrtVQtTNASgVW8zCabZqs2WjYnijMRf1aL4u9VSl+BFMOZUPMYaeBX+u38w==
apollo-server-errors@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.3.tgz#83763b00352c10dc68fbb0d41744ade66de549ff"
integrity sha512-MO4oJ129vuCcbqwr5ZwgxqGGiLz3hCyowz0bstUF7MR+vNGe4oe3DWajC9lv4CxrhcqUHQOeOPViOdIo1IxE3g==
apollo-server-express@^2.9.0, apollo-server-express@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.3.tgz#67573404030c2676be49a7bf97d423b8462e295c"
integrity sha512-Hkfs+ce6GqaoSzDOJs8Pj7W3YUjH0BzGglo5HMsOXOnjPZ0pJE9v8fmK76rlkITLw7GjvIq5GKlafymC31FMBw==
dependencies:
"@apollographql/graphql-playground-html" "1.6.24"
"@types/accepts" "^1.3.5"
@ -1726,10 +1795,11 @@ apollo-server-express@2.9.1, apollo-server-express@^2.9.0:
"@types/cors" "^2.8.4"
"@types/express" "4.17.1"
accepts "^1.3.5"
apollo-server-core "2.9.1"
apollo-server-types "0.2.2"
apollo-server-core "^2.9.3"
apollo-server-types "^0.2.4"
body-parser "^1.18.3"
cors "^2.8.4"
express "^4.17.1"
graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0"
parseurl "^1.3.2"
@ -1743,6 +1813,13 @@ apollo-server-plugin-base@0.6.2:
dependencies:
apollo-server-types "0.2.2"
apollo-server-plugin-base@^0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.4.tgz#63ea4fd0bbb6c4510bc8d0d2ad0a0684c8d0da8c"
integrity sha512-4rY+cBAIpQomGWYBtk8hHkLQWHrh5hgIBPQqmhXh00YFdcY+Ob1/cU2/2iqTcIzhtcaezsc8OZ63au6ahSBQqg==
dependencies:
apollo-server-types "^0.2.4"
apollo-server-testing@~2.9.1:
version "2.9.1"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.1.tgz#29d2524e84722a1319d9c1524b4f9d44379d6a49"
@ -1759,13 +1836,22 @@ apollo-server-types@0.2.2:
apollo-server-caching "0.5.0"
apollo-server-env "2.4.2"
apollo-server@~2.9.1:
version "2.9.1"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.1.tgz#16ff443d43ea38f72fe20adea0803c46037b2b3b"
integrity sha512-iCGoRBOvwTUkDz6Nq/rKguMyhDiQdL3VneF0GTjBGrelTIp3YTIxk/qBFkIr2Chtm9ZZYkS6o+ZldUnxYFKg7A==
apollo-server-types@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.4.tgz#28864900ffc7f9711a859297c143a833fdb6aa43"
integrity sha512-G4FvBVgGQcTW6ZBS2+hvcDQkSfdOIKV+cHADduXA275v+5zl42g+bCaGd/hCCKTDRjmQvObLiMxH/BJ6pDMQgA==
dependencies:
apollo-server-core "2.9.1"
apollo-server-express "2.9.1"
apollo-engine-reporting-protobuf "^0.4.0"
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server@~2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.3.tgz#2a79fcee25da0b0673eb70d73839c40c3c4b8cca"
integrity sha512-JQoeseSo3yOBu3WJzju0NTreoqYckNILybgXNUOhdurE55VFpZ8dsBEO6nMfdO2y1A70W14mnnVWCBEm+1rE8w==
dependencies:
apollo-server-core "^2.9.3"
apollo-server-express "^2.9.3"
express "^4.0.0"
graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0"
@ -1778,6 +1864,14 @@ apollo-tracing@0.8.2:
apollo-server-env "2.4.2"
graphql-extensions "0.10.1"
apollo-tracing@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.4.tgz#0117820c3f0ad3aa6daf7bf13ddbb923cbefa6de"
integrity sha512-DjbFW0IvHicSlTVG+vK+1WINfBMRCdPPHJSW/j65JMir9Oe56WGeqL8qz8hptdUUmLYEb+azvcyyGsJsiR3zpQ==
dependencies:
apollo-server-env "^2.4.3"
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:
version "1.3.2"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
@ -3311,12 +3405,12 @@ eslint-module-utils@^2.4.0:
debug "^2.6.8"
pkg-dir "^2.0.0"
eslint-plugin-es@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6"
integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw==
eslint-plugin-es@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz#12acae0f4953e76ba444bfd1b2271081ac620998"
integrity sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==
dependencies:
eslint-utils "^1.3.0"
eslint-utils "^1.4.2"
regexpp "^2.0.1"
eslint-plugin-import@~2.18.2:
@ -3336,20 +3430,20 @@ eslint-plugin-import@~2.18.2:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.15.2:
version "22.15.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.2.tgz#e3c10d9391f787744e31566f69ebb70c3a98e398"
integrity sha512-p4NME9TgXIt+KgpxcXyNBvO30ZKxwFAO1dJZBc2OGfDnXVEtPwEyNs95GSr6RIE3xLHdjd8ngDdE2icRRXrbxg==
eslint-plugin-jest@~22.16.0:
version "22.16.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.16.0.tgz#30c4e0e9dc331beb2e7369b70dd1363690c1ce05"
integrity sha512-eBtSCDhO1k7g3sULX/fuRK+upFQ7s548rrBtxDyM1fSoY7dTWp/wICjrJcDZKVsW7tsFfH22SG+ZaxG5BZodIg==
dependencies:
"@typescript-eslint/experimental-utils" "^1.13.0"
eslint-plugin-node@~9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a"
integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw==
eslint-plugin-node@~9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.2.0.tgz#b1911f111002d366c5954a6d96d3cd5bf2a3036a"
integrity sha512-2abNmzAH/JpxI4gEOwd6K8wZIodK3BmHbTxz4s79OIYwwIt2gkpEXlAouJXu4H1c9ySTnRso0tsuthSOZbUMlA==
dependencies:
eslint-plugin-es "^1.4.0"
eslint-utils "^1.3.1"
eslint-plugin-es "^1.4.1"
eslint-utils "^1.4.2"
ignore "^5.1.1"
minimatch "^3.0.4"
resolve "^1.10.1"
@ -3388,7 +3482,7 @@ eslint-scope@^5.0.0:
esrecurse "^4.1.0"
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"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
@ -3400,10 +3494,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"
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
eslint@~6.2.2:
version "6.2.2"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.2.2.tgz#03298280e7750d81fcd31431f3d333e43d93f24f"
integrity sha512-mf0elOkxHbdyGX1IJEUsNBzCDdyoUgljF3rRlgfyYh0pwGnreLc0jjD6ZuleOibjmnUWZLY2eXwSooeOgGJ2jw==
eslint@~6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.3.0.tgz#1f1a902f67bfd4c354e7288b81e40654d927eb6a"
integrity sha512-ZvZTKaqDue+N8Y9g0kp6UPZtS4FSY3qARxBs7p4f0H0iof381XHduqVerFWtK8DPtKmemqbqCFENWSQgPR/Gow==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.10.0"
@ -4086,6 +4180,15 @@ graphql-extensions@0.10.1:
apollo-server-env "2.4.2"
apollo-server-types "0.2.2"
graphql-extensions@^0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.10.3.tgz#9e37f3bd26309c40b03a0be0e63e02b3f99d52ea"
integrity sha512-kwU0gUe+Qdfr8iZYT91qrPSwQNgPhB/ClF1m1LEPdxlptk5FhFmjpxAcbMZ8q7j0kjfnbp2IeV1OhRDCEPqz2w==
dependencies:
"@apollographql/apollo-tools" "^0.4.0"
apollo-server-env "^2.4.3"
apollo-server-types "^0.2.4"
graphql-import@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/graphql-import/-/graphql-import-0.7.1.tgz#4add8d91a5f752d764b0a4a7a461fcd93136f223"
@ -4180,10 +4283,10 @@ graphql-upload@^8.0.2:
http-errors "^1.7.2"
object-path "^0.11.4"
graphql@^14.2.1, graphql@^14.5.3:
version "14.5.3"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.3.tgz#e025851cc413e153220f4edbbb25d49f55104fa0"
integrity sha512-W8A8nt9BsMg0ZK2qA3DJIVU6muWhxZRYLTmc+5XGwzWzVdUdPVlAAg5hTBjiTISEnzsKL/onasu6vl3kgGTbYg==
graphql@^14.2.1, graphql@^14.5.4:
version "14.5.4"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.4.tgz#b33fe957854e90c10d4c07c7d26b6c8e9f159a13"
integrity sha512-dPLvHoxy5m9FrkqWczPPRnH0X80CyvRE6e7Fa5AWEqEAzg9LpxHvKh24po/482E6VWHigOkAmb4xCp6P9yT9gw==
dependencies:
iterall "^1.2.2"
@ -6175,12 +6278,12 @@ neo-async@^2.6.0:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.5:
version "1.7.5"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.5.tgz#c3fe3677f69c12f26944563d45e7e7d818a685e4"
integrity sha512-xCD2F5+tp/SD9r5avX5bSoY8u8RH2o793xJ9Ikjz1s5qQy7cFxFbbj2c52uz3BVGhRAx/NmB57VjOquYmmxGtw==
neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.6.tgz#eccb135a71eba9048c68717444593a6424cffc49"
integrity sha512-6c3ALO3vYDfUqNoCy8OFzq+fQ7q/ab3LCuJrmm8P04M7RmyRCCnUtJ8IzSTGbiZvyhcehGK+azNDAEJhxPV/hA==
dependencies:
"@babel/runtime" "^7.4.4"
"@babel/runtime" "^7.5.5"
text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2"

View File

@ -168,13 +168,13 @@ Given("we have the following posts in our database:", table => {
};
postAttributes.deleted = Boolean(postAttributes.deleted);
const disabled = Boolean(postAttributes.disabled);
postAttributes.categoryIds = [`cat${i}`];
postAttributes.categoryIds = [`cat${i}${new Date()}`];
postAttributes;
cy.factory()
.create("User", userAttributes)
.authenticateAs(userAttributes)
.create("Category", {
id: `cat${i}`,
id: `cat${i}${new Date()}`,
name: "Just For Fun",
slug: `just-for-fun-${i}`,
icon: "smile"
@ -357,7 +357,7 @@ When("mention {string} in the text", mention => {
});
Then("the notification gets marked as read", () => {
cy.get(".post.createdAt")
cy.get(".notifications-menu-popover .notification")
.first()
.should("have.class", "read");
});

View File

@ -26,6 +26,9 @@ Feature: Block a User
And nobody is following the user profile anymore
Scenario: Posts of blocked users are filtered from search results
Given we have the following posts in our database:
| Author | id | title | content |
| Some unblocked user | im-not-blocked | Post that should be seen | cause I'm not blocked |
Given "Spammy Spammer" wrote a post "Spam Spam Spam"
When I search for "Spam"
Then I should see the following posts in the select dropdown:
@ -35,3 +38,7 @@ Feature: Block a User
And I refresh the page
And I search for "Spam"
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 |

View File

@ -30,17 +30,6 @@
memory: "1G"
limits:
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:
- configMapRef:
name: configmap

View File

@ -9,6 +9,11 @@
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
NEO4J_AUTH: "none"
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_BACKEND: ""
COMMIT: ""

View File

@ -137,8 +137,8 @@ p.contentExcerpt = post.contentExcerpt,
p.visibility = toLower(post.visibility),
p.createdAt = post.createdAt.`$date`,
p.updatedAt = post.updatedAt.`$date`,
p.deleted = COALESCE(post.deleted,false),
p.disabled = NOT post.isEnabled
p.deleted = COALESCE(post.deleted, false),
p.disabled = COALESCE(NOT post.isEnabled, false)
WITH p, post
MATCH (u:User {id: post.userId})
MERGE (u)-[:WROTE]->(p)

View File

@ -29,7 +29,7 @@
"dotenv": "^8.1.0",
"faker": "Marak/faker.js#master",
"graphql-request": "^1.8.2",
"neo4j-driver": "^1.7.5",
"neo4j-driver": "^1.7.6",
"neode": "^0.3.2",
"npm-run-all": "^4.1.5",
"slug": "^1.1.0"

View File

@ -37,9 +37,9 @@ describe('Notification', () => {
describe('given a notification about a comment on a post', () => {
beforeEach(() => {
propsData.notification = {
reason: 'comment_on_post',
post: null,
comment: {
reason: 'commented_on_post',
from: {
__typename: 'Comment',
id: 'comment-1',
contentExcerpt:
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
@ -56,7 +56,7 @@ describe('Notification', () => {
it('renders reason', () => {
wrapper = Wrapper()
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
'notifications.menu.comment_on_post',
'notifications.menu.commented_on_post',
)
})
it('renders title', () => {
@ -92,14 +92,14 @@ describe('Notification', () => {
beforeEach(() => {
propsData.notification = {
reason: 'mentioned_in_post',
post: {
from: {
__typename: 'Post',
title: "It's a post title",
id: 'post-1',
slug: 'its-a-title',
contentExcerpt:
'<a href="/profile/u3" target="_blank">@jenny-rostock</a> is the best on this post.',
},
comment: null,
}
})
@ -138,8 +138,8 @@ describe('Notification', () => {
beforeEach(() => {
propsData.notification = {
reason: 'mentioned_in_comment',
post: null,
comment: {
from: {
__typename: 'Comment',
id: 'comment-1',
contentExcerpt:
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',

View File

@ -1,14 +1,8 @@
<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>
<ds-space margin-bottom="x-small">
<hc-user
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" />
<hc-user :user="from.author" :date-time="from.createdAt" :trunc="35" />
</ds-space>
<ds-text class="reason-text-for-test" color="soft">
{{ $t(`notifications.menu.${notification.reason}`) }}
@ -22,16 +16,15 @@
>
<ds-space margin-bottom="x-small">
<ds-card
:header="post.title || comment.post.title"
:header="from.title || from.post.title"
hover
space="x-small"
class="notifications-card"
>
<ds-space margin-bottom="x-small" />
<div v-if="resourceType == 'Post'">{{ post.contentExcerpt | removeHtml }}</div>
<div v-else>
<span class="comment-notification-header">Comment:</span>
{{ comment.contentExcerpt | removeHtml }}
<div>
<span v-if="isComment" class="comment-notification-header">Comment:</span>
{{ from.contentExcerpt | removeHtml }}
</div>
</ds-card>
</ds-space>
@ -54,23 +47,21 @@ export default {
},
},
computed: {
resourceType() {
return this.post.id ? 'Post' : 'Comment'
from() {
return this.notification.from
},
post() {
return this.notification.post || {}
},
comment() {
return this.notification.comment || {}
isComment() {
return this.from.__typename === 'Comment'
},
params() {
const post = this.isComment ? this.from.post : this.from
return {
id: this.post.id || this.comment.post.id,
slug: this.post.slug || this.comment.post.slug,
id: post.id,
slug: post.slug,
}
},
hashParam() {
return this.post.id ? {} : { hash: `#commentId-${this.comment.id}` }
return this.isComment ? { hash: `#commentId-${this.from.id}` } : {}
},
},
}

View File

@ -40,9 +40,9 @@ describe('NotificationList.vue', () => {
propsData = {
notifications: [
{
id: 'notification-41',
read: false,
post: {
from: {
__typename: 'Post',
id: 'post-1',
title: 'some post title',
slug: 'some-post-title',
@ -55,9 +55,9 @@ describe('NotificationList.vue', () => {
},
},
{
id: 'notification-42',
read: false,
post: {
from: {
__typename: 'Post',
id: 'post-2',
title: 'another post title',
slug: 'another-post-title',
@ -115,9 +115,9 @@ describe('NotificationList.vue', () => {
.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')[0]).toEqual(['notification-42'])
expect(wrapper.emitted('markAsRead')[0]).toEqual(['post-2'])
})
})
})

View File

@ -4,7 +4,7 @@
v-for="notification in notifications"
:key="notification.id"
:notification="notification"
@read="markAsRead(notification.id)"
@read="markAsRead(notification.from.id)"
/>
</div>
</template>
@ -24,8 +24,8 @@ export default {
},
},
methods: {
markAsRead(notificationId) {
this.$emit('markAsRead', notificationId)
markAsRead(notificationSourceId) {
this.$emit('markAsRead', notificationSourceId)
},
},
}

View File

@ -18,7 +18,7 @@
<script>
import Dropdown from '~/components/Dropdown'
import { currentUserNotificationsQuery, updateNotificationMutation } from '~/graphql/User'
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
import NotificationList from '../NotificationList/NotificationList'
export default {
@ -27,36 +27,41 @@ export default {
NotificationList,
Dropdown,
},
data() {
return {
notifications: [],
}
},
props: {
placement: { type: String },
},
computed: {
totalNotifications() {
return (this.notifications || []).length
},
},
methods: {
async markAsRead(notificationId) {
const variables = { id: notificationId, read: true }
async markAsRead(notificationSourceId) {
const variables = { id: notificationSourceId }
try {
await this.$apollo.mutate({
mutation: updateNotificationMutation(),
const {
data: { markAsRead },
} = await this.$apollo.mutate({
mutation: markAsReadMutation(),
variables,
})
if (!(markAsRead && markAsRead.read === true)) return
this.notifications = this.notifications.map(n => {
return n.from.id === markAsRead.from.id ? markAsRead : n
})
} catch (err) {
throw new Error(err)
}
},
},
computed: {
totalNotifications() {
return this.notifications.length
},
},
apollo: {
notifications: {
query: currentUserNotificationsQuery(),
update: data => {
const {
currentUser: { notifications },
} = data
return notifications
},
query: notificationQuery(),
},
},
}

View File

@ -1,5 +1,58 @@
import gql from 'graphql-tag'
const fragments = gql`
fragment post on Post {
id
createdAt
disabled
deleted
title
contentExcerpt
slug
author {
id
slug
name
disabled
deleted
avatar
}
}
fragment comment on Comment {
id
createdAt
disabled
deleted
contentExcerpt
author {
id
slug
name
disabled
deleted
avatar
}
post {
id
createdAt
disabled
deleted
title
contentExcerpt
slug
author {
id
slug
name
disabled
deleted
avatar
}
}
}
`
export default i18n => {
const lang = i18n.locale().toUpperCase()
return gql`
@ -76,77 +129,37 @@ export default i18n => {
`
}
export const currentUserNotificationsQuery = () => {
export const notificationQuery = () => {
return gql`
${fragments}
query {
currentUser {
id
notifications(read: false, orderBy: createdAt_desc) {
id
read
reason
createdAt
post {
id
createdAt
disabled
deleted
title
contentExcerpt
slug
author {
id
slug
name
disabled
deleted
avatar
}
}
comment {
id
createdAt
disabled
deleted
contentExcerpt
author {
id
slug
name
disabled
deleted
avatar
}
post {
id
createdAt
disabled
deleted
title
contentExcerpt
slug
author {
id
slug
name
disabled
deleted
avatar
}
}
}
notifications(read: false, orderBy: createdAt_desc) {
read
reason
createdAt
from {
__typename
...post
...comment
}
}
}
`
}
export const updateNotificationMutation = () => {
export const markAsReadMutation = () => {
return gql`
mutation($id: ID!, $read: Boolean!) {
UpdateNotification(id: $id, read: $read) {
id
${fragments}
mutation($id: ID!) {
markAsRead(id: $id) {
read
reason
createdAt
from {
__typename
...post
...comment
}
}
}
`

View File

@ -134,7 +134,7 @@
"menu": {
"mentioned_in_post": "Hat dich in einem Beitrag 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": {

View File

@ -134,7 +134,7 @@
"menu": {
"mentioned_in_post": "Mentioned you in a post …",
"mentioned_in_comment": "Mentioned you in a comment …",
"comment_on_post": "Commented on your post …"
"commented_on_post": "Commented on your post …"
}
},
"search": {

View File

@ -63,7 +63,7 @@
"cross-env": "~5.2.0",
"date-fns": "2.0.1",
"express": "~4.17.1",
"graphql": "~14.5.3",
"graphql": "~14.5.4",
"isemail": "^3.2.0",
"jsonwebtoken": "~8.5.1",
"linkify-it": "~2.2.0",
@ -78,7 +78,7 @@
"v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13",
"vue-infinite-scroll": "^2.0.2",
"vue-izitoast": "^1.2.0",
"vue-izitoast": "^1.2.1",
"vue-sweetalert-icons": "~4.2.0",
"vuex-i18n": "~1.13.1",
"xregexp": "^4.2.4",
@ -107,8 +107,8 @@
"eslint-config-standard": "~12.0.0",
"eslint-loader": "~3.0.0",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.15.2",
"eslint-plugin-node": "~9.1.0",
"eslint-plugin-jest": "~22.16.0",
"eslint-plugin-node": "~9.2.0",
"eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1",
@ -120,7 +120,7 @@
"node-sass": "~4.12.0",
"nodemon": "~1.19.1",
"prettier": "~1.18.2",
"sass-loader": "~7.3.1",
"sass-loader": "~8.0.0",
"style-loader": "~0.23.1",
"style-resources-loader": "~1.2.1",
"tippy.js": "^4.3.5",

View File

@ -185,9 +185,6 @@ export default {
return result
},
update({ Post }) {
// 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.hasMore = Post.length >= this.pageSize
const posts = uniqBy([...this.posts, ...Post], 'id')
this.posts = posts

View File

@ -104,9 +104,7 @@ describe('ProfileSlug', () => {
describe('currently no posts available (e.g. after tab switching)', () => {
beforeEach(() => {
wrapper.setData({
Post: null,
})
wrapper.setData({ posts: [], hasMore: false })
})
it('displays no "load more" button', () => {
@ -137,9 +135,7 @@ describe('ProfileSlug', () => {
}
})
wrapper.setData({
Post: posts,
})
wrapper.setData({ posts, hasMore: true })
})
it('displays a "load more" button', () => {
@ -170,9 +166,7 @@ describe('ProfileSlug', () => {
}
})
wrapper.setData({
Post: posts,
})
wrapper.setData({ posts, hasMore: false })
})
it('displays no "load more" button', () => {

View File

@ -219,8 +219,8 @@
</ds-space>
</ds-grid-item>
<template v-if="activePosts.length">
<masonry-grid-item v-for="(post, index) in activePosts" :key="post.id">
<template v-if="posts.length">
<masonry-grid-item v-for="(post, index) in posts" :key="post.id">
<hc-post-card
:post="post"
:width="{ base: '100%', md: '100%', xl: '50%' }"
@ -229,10 +229,10 @@
</masonry-grid-item>
</template>
<template v-else-if="$apollo.loading">
<ds-grid-item>
<ds-section centered>
<ds-grid-item column-span="fullWidth">
<ds-space centered>
<ds-spinner size="base"></ds-spinner>
</ds-section>
</ds-space>
</ds-grid-item>
</template>
<template v-else>
@ -306,33 +306,21 @@ export default {
const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
return {
User: [],
Post: [],
activePosts: [],
voted: false,
page: 1,
posts: [],
hasMore: false,
offset: 0,
pageSize: 6,
tabActive: 'post',
filter,
}
},
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() {
return this.$route.params.id === this.$store.getters['auth/user'].id
},
user() {
return this.User ? this.User[0] : {}
},
offset() {
return (this.page - 1) * this.pageSize
},
socialMediaLinks() {
const { socialMedia = [] } = this.user
return socialMedia.map(socialMedia => {
@ -355,19 +343,15 @@ export default {
throw new Error('User not found!')
}
},
Post(val) {
this.activePosts = this.setActivePosts()
},
},
methods: {
removePostFromList(index) {
this.activePosts.splice(index, 1)
this.$apollo.queries.User.refetch()
this.posts.splice(index, 1)
},
handleTab(tab) {
this.tabActive = tab
this.Post = null
this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
this.resetPostList()
},
uniq(items, field = 'id') {
return uniqBy(items, field)
@ -377,38 +361,23 @@ export default {
this.$apollo.queries.User.refetch()
},
showMoreContributions() {
// this.page++
// 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',
})
this.offset += this.pageSize
},
setActivePosts() {
if (!this.Post) {
return []
}
return this.uniq(this.Post.filter(post => !post.deleted))
resetPostList() {
this.offset = 0
this.posts = []
this.hasMore = false
},
async block(user) {
await this.$apollo.mutate({ mutation: Block(), variables: { id: user.id } })
this.$apollo.queries.User.refetch()
this.resetPostList()
this.$apollo.queries.Post.refetch()
},
async unblock(user) {
await this.$apollo.mutate({ mutation: Unblock(), variables: { id: user.id } })
this.$apollo.queries.User.refetch()
this.resetPostList()
this.$apollo.queries.Post.refetch()
},
},
@ -421,10 +390,19 @@ export default {
return {
filter: this.filter,
first: this.pageSize,
offset: 0,
offset: this.offset,
orderBy: 'createdAt_desc',
}
},
fetchPolicy: 'cache-and-network',
update({ Post }) {
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.hasMore = Post.length >= this.pageSize
this.posts = this.uniq([...this.posts, ...Post])
},
},
User: {
query() {

View File

@ -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 }) => {
const backendUrl = process.env.GRAPHQL_URI || 'http://localhost:4000'
@ -10,5 +17,6 @@ export default ({ app }) => {
tokenName: 'human-connection-token',
persisting: false,
websocketsOnly: false,
cache: new InMemoryCache({ fragmentMatcher }),
}
}

View File

@ -0,0 +1,18 @@
{
"__schema": {
"types": [
{
"kind": "UNION",
"name": "NotificationSource",
"possibleTypes": [
{
"name": "Post"
},
{
"name": "Comment"
}
]
}
]
}
}

View File

@ -86,23 +86,6 @@ export const actions = {
id
url
}
notifications(read: false, orderBy: createdAt_desc) {
id
read
createdAt
post {
author {
id
slug
name
disabled
deleted
}
title
contentExcerpt
slug
}
}
}
}
`,

View File

@ -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)
},
}

View File

@ -46,8 +46,8 @@ export const actions = {
await this.app.apolloProvider.defaultClient
.query({
query: gql`
query findPosts($query: String!) {
findPosts(query: $query, limit: 10) {
query findPosts($query: String!, $filter: _PostFilter) {
findPosts(query: $query, limit: 10, filter: $filter) {
id
slug
label: title
@ -64,6 +64,7 @@ export const actions = {
`,
variables: {
query: value.replace(/\s/g, '~ ') + '~',
filter: {},
},
})
.then(res => {

View File

@ -6387,12 +6387,12 @@ eslint-module-utils@^2.4.0:
debug "^2.6.8"
pkg-dir "^2.0.0"
eslint-plugin-es@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6"
integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw==
eslint-plugin-es@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz#12acae0f4953e76ba444bfd1b2271081ac620998"
integrity sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==
dependencies:
eslint-utils "^1.3.0"
eslint-utils "^1.4.2"
regexpp "^2.0.1"
eslint-plugin-import@~2.18.2:
@ -6412,20 +6412,20 @@ eslint-plugin-import@~2.18.2:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.15.2:
version "22.15.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.2.tgz#e3c10d9391f787744e31566f69ebb70c3a98e398"
integrity sha512-p4NME9TgXIt+KgpxcXyNBvO30ZKxwFAO1dJZBc2OGfDnXVEtPwEyNs95GSr6RIE3xLHdjd8ngDdE2icRRXrbxg==
eslint-plugin-jest@~22.16.0:
version "22.16.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.16.0.tgz#30c4e0e9dc331beb2e7369b70dd1363690c1ce05"
integrity sha512-eBtSCDhO1k7g3sULX/fuRK+upFQ7s548rrBtxDyM1fSoY7dTWp/wICjrJcDZKVsW7tsFfH22SG+ZaxG5BZodIg==
dependencies:
"@typescript-eslint/experimental-utils" "^1.13.0"
eslint-plugin-node@~9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a"
integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw==
eslint-plugin-node@~9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.2.0.tgz#b1911f111002d366c5954a6d96d3cd5bf2a3036a"
integrity sha512-2abNmzAH/JpxI4gEOwd6K8wZIodK3BmHbTxz4s79OIYwwIt2gkpEXlAouJXu4H1c9ySTnRso0tsuthSOZbUMlA==
dependencies:
eslint-plugin-es "^1.4.0"
eslint-utils "^1.3.1"
eslint-plugin-es "^1.4.1"
eslint-utils "^1.4.2"
ignore "^5.1.1"
minimatch "^3.0.4"
resolve "^1.10.1"
@ -6463,7 +6463,7 @@ eslint-scope@^4.0.0, eslint-scope@^4.0.3:
esrecurse "^4.1.0"
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"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
@ -7635,10 +7635,10 @@ graphql-upload@^8.0.2:
http-errors "^1.7.2"
object-path "^0.11.4"
"graphql@14.0.2 - 14.2.0 || ^14.3.1", graphql@^14.4.0, graphql@~14.5.3:
version "14.5.3"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.3.tgz#e025851cc413e153220f4edbbb25d49f55104fa0"
integrity sha512-W8A8nt9BsMg0ZK2qA3DJIVU6muWhxZRYLTmc+5XGwzWzVdUdPVlAAg5hTBjiTISEnzsKL/onasu6vl3kgGTbYg==
"graphql@14.0.2 - 14.2.0 || ^14.3.1", graphql@^14.4.0, graphql@~14.5.4:
version "14.5.4"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.4.tgz#b33fe957854e90c10d4c07c7d26b6c8e9f159a13"
integrity sha512-dPLvHoxy5m9FrkqWczPPRnH0X80CyvRE6e7Fa5AWEqEAzg9LpxHvKh24po/482E6VWHigOkAmb4xCp6P9yT9gw==
dependencies:
iterall "^1.2.2"
@ -8162,12 +8162,7 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.1.tgz#2fc6b8f518aff48fef65a7f348ed85632448e4a5"
integrity sha512-DWjnQIFLenVrwyRCKZT+7a7/U4Cqgar4WG8V++K3hw+lrW1hc/SIwdiGmtxKCVACmHULTuGeBbHJmbwW7/sAvA==
ignore@^5.1.4:
ignore@^5.1.1, ignore@^5.1.4:
version "5.1.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
@ -13345,7 +13340,7 @@ sass-graph@^2.2.4:
scss-tokenizer "^0.2.3"
yargs "^7.0.0"
sass-loader@^7.1.0, sass-loader@~7.3.1:
sass-loader@^7.1.0:
version "7.3.1"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.3.1.tgz#a5bf68a04bcea1c13ff842d747150f7ab7d0d23f"
integrity sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==
@ -13356,6 +13351,17 @@ sass-loader@^7.1.0, sass-loader@~7.3.1:
pify "^4.0.1"
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:
version "2.0.0"
resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-2.0.0.tgz#88569c542fbf1f18f33a6578b77cc5b36c56911d"
@ -15173,10 +15179,10 @@ vue-infinite-scroll@^2.0.2:
resolved "https://registry.yarnpkg.com/vue-infinite-scroll/-/vue-infinite-scroll-2.0.2.tgz#ca37a91fe92ee0ad3b74acf8682c00917144b711"
integrity sha512-n+YghR059YmciANGJh9SsNWRi1YZEBVlODtmnb/12zI+4R72QZSWd+EuZ5mW6auEo/yaJXgxzwsuhvALVnm73A==
vue-izitoast@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/vue-izitoast/-/vue-izitoast-1.2.0.tgz#55b7434a391c6eb64dd10c0de211e99ba7e486e2"
integrity sha512-Jqxfid12SUBIySJxgyPpu6gZ1ssMcbKtCvu9uMQPNM8RUnd3RKC4nyxkncdYe5L6XPU+SaznjYRudnvtclY4wA==
vue-izitoast@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/vue-izitoast/-/vue-izitoast-1.2.1.tgz#cd2cbfbd96ea438dede8fb00f2c328364cb7141d"
integrity sha512-5krrKyAftSR3TnnO3zhMihYCSt0Lay4SBO1AWWKD3jhTErJrR+q9kOKyuAYhn1SttNER87hpnRKqdvLjzjHWQQ==
dependencies:
izitoast "^1.4.0"

View File

@ -740,10 +740,10 @@
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.4.4":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
"@babel/runtime@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
dependencies:
regenerator-runtime "^0.13.2"
@ -3463,12 +3463,12 @@ needle@^2.2.1:
iconv-lite "^0.4.4"
sax "^1.2.4"
neo4j-driver@^1.7.5:
version "1.7.5"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.5.tgz#c3fe3677f69c12f26944563d45e7e7d818a685e4"
integrity sha512-xCD2F5+tp/SD9r5avX5bSoY8u8RH2o793xJ9Ikjz1s5qQy7cFxFbbj2c52uz3BVGhRAx/NmB57VjOquYmmxGtw==
neo4j-driver@^1.7.5, neo4j-driver@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.6.tgz#eccb135a71eba9048c68717444593a6424cffc49"
integrity sha512-6c3ALO3vYDfUqNoCy8OFzq+fQ7q/ab3LCuJrmm8P04M7RmyRCCnUtJ8IzSTGbiZvyhcehGK+azNDAEJhxPV/hA==
dependencies:
"@babel/runtime" "^7.4.4"
"@babel/runtime" "^7.5.5"
text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2"