From 643d175ef623dcf512d609cfd401e937aad54369 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Thu, 29 Aug 2019 15:30:19 +0200 Subject: [PATCH 01/17] Implement notifications resolver --- .../notifications/notificationsMiddleware.js | 9 ++--- .../src/middleware/permissionsMiddleware.js | 1 + backend/src/models/Notification.js | 36 ------------------- backend/src/models/index.js | 1 - backend/src/schema/resolvers/notifications.js | 30 ++++++++++++++++ backend/src/schema/types/type/NOTIFIED.gql | 12 +++++++ 6 files changed, 46 insertions(+), 43 deletions(-) delete mode 100644 backend/src/models/Notification.js create mode 100644 backend/src/schema/types/type/NOTIFIED.gql diff --git a/backend/src/middleware/notifications/notificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js index c9dfe406c..56c7366d8 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -25,8 +25,7 @@ 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)-[:NOTIFIED {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }]->(user) ` break } @@ -37,8 +36,7 @@ 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)-[:NOTIFIED {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }]->(user) ` break } @@ -49,8 +47,7 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { 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)-[:NOTIFIED {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }]->(user) ` break } diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 83c29d19d..2b29cd152 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -160,6 +160,7 @@ const permissions = shield( PostsEmotionsCountByEmotion: allow, PostsEmotionsByCurrentUser: allow, blockedUsers: isAuthenticated, + notifications: isAuthenticated, }, Mutation: { '*': deny, diff --git a/backend/src/models/Notification.js b/backend/src/models/Notification.js deleted file mode 100644 index b54a99574..000000000 --- a/backend/src/models/Notification.js +++ /dev/null @@ -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', - }, -} diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 295082de4..5a4510ac6 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -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'), } diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index ddc1985cf..fcb8db1a4 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -2,6 +2,36 @@ import { neo4jgraphql } from 'neo4j-graphql-js' export default { Query: { + notifications: async (parent, params, context, resolveInfo) => { + const { user, driver } = context + let session + let notifications + try { + session = context.driver.session() + const cypher = ` + MATCH (resource)-[notification:NOTIFIED]->(user:User {id:$id}) + RETURN resource, notification, user + ` + const result = await session.run(cypher, { id: user.id }) + const resourceTypes = ['Post', 'Comment'] + notifications = await result.records.map(record => { + return { + ...record.get('notification').properties, + from: { + __typename: record.get('resource').labels.find(l => resourceTypes.includes(l)), + ...record.get('resource').properties + }, + to: { + __typename: 'User', + ...record.get('user').properties, + } + } + }) + } finally { + session.close() + } + return notifications + }, Notification: (object, params, context, resolveInfo) => { return neo4jgraphql(object, params, context, resolveInfo, false) }, diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/schema/types/type/NOTIFIED.gql new file mode 100644 index 000000000..40a6f1b22 --- /dev/null +++ b/backend/src/schema/types/type/NOTIFIED.gql @@ -0,0 +1,12 @@ +type NOTIFIED { + from: NotificationSource + to: User + createdAt: String + read: Boolean +} + +union NotificationSource = Post | Comment + +type Query { + notifications: [NOTIFIED] +} From 4b96454b90568ffc9d8067ac89caf97d2e1fb89b Mon Sep 17 00:00:00 2001 From: roschaefer Date: Thu, 29 Aug 2019 19:12:20 +0200 Subject: [PATCH 02/17] Notifications resolver capable of orderBy + filter --- backend/src/middleware/dateTimeMiddleware.js | 2 -- .../src/middleware/permissionsMiddleware.js | 3 +- backend/src/schema/index.js | 2 ++ backend/src/schema/resolvers/notifications.js | 36 +++++++++++++++---- .../schema/resolvers/notifications.spec.js | 36 ++++++++----------- backend/src/schema/types/type/NOTIFIED.gql | 13 +++++-- .../src/schema/types/type/Notification.gql | 9 ----- backend/src/schema/types/type/User.gql | 2 -- backend/src/seed/factories/index.js | 2 -- backend/src/seed/factories/notifications.js | 17 --------- 10 files changed, 57 insertions(+), 65 deletions(-) delete mode 100644 backend/src/schema/types/type/Notification.gql delete mode 100644 backend/src/seed/factories/notifications.js diff --git a/backend/src/middleware/dateTimeMiddleware.js b/backend/src/middleware/dateTimeMiddleware.js index c8af53a7a..ff1fcc996 100644 --- a/backend/src/middleware/dateTimeMiddleware.js +++ b/backend/src/middleware/dateTimeMiddleware.js @@ -12,11 +12,9 @@ export default { CreatePost: setCreatedAt, CreateComment: setCreatedAt, CreateOrganization: setCreatedAt, - CreateNotification: setCreatedAt, UpdateUser: setUpdatedAt, UpdatePost: setUpdatedAt, UpdateComment: setUpdatedAt, UpdateOrganization: setUpdatedAt, - UpdateNotification: setUpdatedAt, }, } diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 2b29cd152..6f8ff8c2d 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -149,7 +149,6 @@ const permissions = shield( Category: allow, Tag: allow, Report: isModerator, - Notification: isAdmin, statistics: allow, currentUser: allow, Post: or(onlyEnabledContent, isModerator), @@ -169,7 +168,6 @@ const permissions = shield( Signup: isAdmin, SignupVerification: allow, CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)), - UpdateNotification: belongsToMe, UpdateUser: onlyYourself, CreatePost: isAuthenticated, UpdatePost: isAuthor, @@ -199,6 +197,7 @@ const permissions = shield( RemovePostEmotions: isAuthenticated, block: isAuthenticated, unblock: isAuthenticated, + markAsRead: belongsToMe }, User: { email: isMyOwn, diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index b8f120057..e8fa63d97 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -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 }, diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index fcb8db1a4..17b8e7532 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -2,15 +2,40 @@ import { neo4jgraphql } from 'neo4j-graphql-js' export default { Query: { - notifications: async (parent, params, context, resolveInfo) => { + notifications: async (parent, args, context, resolveInfo) => { const { user, driver } = context let 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 { session = context.driver.session() const cypher = ` MATCH (resource)-[notification:NOTIFIED]->(user:User {id:$id}) + ${whereClause} RETURN resource, notification, user + ${orderByClause} ` const result = await session.run(cypher, { id: user.id }) const resourceTypes = ['Post', 'Comment'] @@ -32,13 +57,10 @@ export default { } return notifications }, - Notification: (object, params, context, resolveInfo) => { - return neo4jgraphql(object, params, context, resolveInfo, false) - }, }, Mutation: { - UpdateNotification: (object, params, context, resolveInfo) => { - return neo4jgraphql(object, params, context, resolveInfo, false) - }, + markAsRead: async (parent, params, context, resolveInfo) => { + return null + } }, } diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 3ca7727e4..c173eeef1 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -26,10 +26,10 @@ afterEach(async () => { await factory.cleanDatabase() }) -describe('Notification', () => { +describe('notifications', () => { const notificationQuery = gql` query { - Notification { + notifications { id } } @@ -41,10 +41,6 @@ describe('Notification', () => { await expect(client.request(notificationQuery)).rejects.toThrow('Not Authorised') }) }) -}) - -describe('currentUser notifications', () => { - const variables = {} describe('authenticated', () => { let headers @@ -160,15 +156,13 @@ describe('currentUser notifications', () => { describe('filter for read: false', () => { const queryCurrentUserNotificationsFilterRead = gql` query($read: Boolean) { - currentUser { - notifications(read: $read, orderBy: createdAt_desc) { + notifications(read: $read, orderBy: createdAt_desc) { + id + post { + id + } + comment { id - post { - id - } - comment { - id - } } } } @@ -204,15 +198,13 @@ describe('currentUser notifications', () => { describe('no filters', () => { const queryCurrentUserNotifications = gql` query { - currentUser { - notifications(orderBy: createdAt_desc) { + notifications(orderBy: createdAt_desc) { + id + post { + id + } + comment { id - post { - id - } - comment { - id - } } } } diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/schema/types/type/NOTIFIED.gql index 40a6f1b22..0be774f30 100644 --- a/backend/src/schema/types/type/NOTIFIED.gql +++ b/backend/src/schema/types/type/NOTIFIED.gql @@ -7,6 +7,15 @@ type NOTIFIED { union NotificationSource = Post | Comment -type Query { - notifications: [NOTIFIED] +enum NOTIFIEDOrdering { + createdAt_asc + createdAt_desc +} + +type Query { + notifications(read: Boolean, orderBy: NOTIFIEDOrdering): [NOTIFIED] +} + +type Mutation { + markAsRead(id: ID!): NOTIFIED } diff --git a/backend/src/schema/types/type/Notification.gql b/backend/src/schema/types/type/Notification.gql deleted file mode 100644 index a3543445f..000000000 --- a/backend/src/schema/types/type/Notification.gql +++ /dev/null @@ -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") -} diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 49592e8ba..9bbef50e4 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -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)") diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index 56518bd06..4cc143e68 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -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 = {}) => { diff --git a/backend/src/seed/factories/notifications.js b/backend/src/seed/factories/notifications.js deleted file mode 100644 index d14d4294a..000000000 --- a/backend/src/seed/factories/notifications.js +++ /dev/null @@ -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 }, - } -} From cf8ead10f3c2a41669cee8339360c41bf08fb33a Mon Sep 17 00:00:00 2001 From: roschaefer Date: Thu, 29 Aug 2019 20:36:30 +0200 Subject: [PATCH 03/17] Start to refactor backend tests --- .../src/middleware/permissionsMiddleware.js | 2 +- backend/src/schema/resolvers/notifications.js | 25 +- .../schema/resolvers/notifications.spec.js | 310 ++++++++---------- 3 files changed, 149 insertions(+), 188 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 6f8ff8c2d..b40e7bc99 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -197,7 +197,7 @@ const permissions = shield( RemovePostEmotions: isAuthenticated, block: isAuthenticated, unblock: isAuthenticated, - markAsRead: belongsToMe + markAsRead: belongsToMe, }, User: { email: isMyOwn, diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index 17b8e7532..6711f6fdd 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -1,36 +1,33 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' - export default { Query: { notifications: async (parent, args, context, resolveInfo) => { - const { user, driver } = context - let session + const { user } = context + const session = context.driver.session() let notifications let whereClause let orderByClause - switch(args.read) { + switch (args.read) { case true: whereClause = 'WHERE notification.read = TRUE' - break; + break case false: whereClause = 'WHERE notification.read = FALSE' - break; + break default: whereClause = '' } - switch(args.orderBy) { + switch (args.orderBy) { case 'createdAt_asc': orderByClause = 'ORDER BY notification.createdAt ASC' - break; + break case 'createdAt_desc': orderByClause = 'ORDER BY notification.createdAt DESC' - break; + break default: orderByClause = '' } try { - session = context.driver.session() const cypher = ` MATCH (resource)-[notification:NOTIFIED]->(user:User {id:$id}) ${whereClause} @@ -44,12 +41,12 @@ export default { ...record.get('notification').properties, from: { __typename: record.get('resource').labels.find(l => resourceTypes.includes(l)), - ...record.get('resource').properties + ...record.get('resource').properties, }, to: { __typename: 'User', ...record.get('user').properties, - } + }, } }) } finally { @@ -61,6 +58,6 @@ export default { Mutation: { markAsRead: async (parent, params, context, resolveInfo) => { return null - } + }, }, } diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index c173eeef1..72a9a8355 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,25 +1,41 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' import { host, login, gql } from '../../jest/helpers' -import { neode } from '../../bootstrap/neo4j' +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 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 () => { @@ -28,129 +44,132 @@ afterEach(async () => { describe('notifications', () => { const notificationQuery = gql` - query { - notifications { - id + query($read: Boolean, $orderBy: NOTIFIEDOrdering) { + notifications(read: $read, orderBy: $orderBy) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt } } ` + 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-31T17: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('unauthenticated', () => { it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(notificationQuery)).rejects.toThrow('Not Authorised') + const result = await query({ query: notificationQuery }) + expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') }) }) describe('authenticated', () => { - let headers beforeEach(async () => { - headers = await login({ - email: 'test@example.org', - password: '1234', - }) - client = new GraphQLClient(host, { - headers, - }) + const user = await factory.create('User', userParams) + authenticatedUser = await user.toJson() }) 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', - }), - ]) + await factory.create('User', { id: 'neighbor' }) + await Promise.all(setupNotifications.map(s => neode.cypher(s))) + }) + + 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-31T17: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) + }) }) describe('filter for read: false', () => { @@ -167,7 +186,7 @@ describe('notifications', () => { } } ` - const variables = { read: false } + it('returns only unread notifications of current user', async () => { const expected = { currentUser: { @@ -194,61 +213,6 @@ describe('notifications', () => { ).resolves.toEqual(expected) }) }) - - describe('no filters', () => { - const queryCurrentUserNotifications = gql` - query { - 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, - ) - }) - }) }) }) }) From 2846a6a8d3a09c23256e82c96120fe6cd8b264e8 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Thu, 29 Aug 2019 21:56:56 +0200 Subject: [PATCH 04/17] Spec for notifications passing --- .../src/middleware/permissionsMiddleware.js | 28 +- backend/src/schema/resolvers/notifications.js | 32 +- .../schema/resolvers/notifications.spec.js | 388 ++++++++---------- backend/src/schema/types/type/NOTIFIED.gql | 11 +- 4 files changed, 205 insertions(+), 254 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index b40e7bc99..2a52e54af 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -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 @@ -197,7 +171,7 @@ const permissions = shield( RemovePostEmotions: isAuthenticated, block: isAuthenticated, unblock: isAuthenticated, - markAsRead: belongsToMe, + markAsRead: isAuthenticated, }, User: { email: isMyOwn, diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index 6711f6fdd..e46b61ba4 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -56,8 +56,36 @@ export default { }, }, Mutation: { - markAsRead: async (parent, params, context, resolveInfo) => { - return null + markAsRead: async (parent, args, context, resolveInfo) => { + const { user } = context + const session = context.driver.session() + let notification + try { + const cypher = ` + MATCH (resource {id: $resourceId})-[notification:NOTIFIED]->(user:User {id:$id}) + SET notification.read = TRUE + RETURN resource, notification, user + ` + const result = await session.run(cypher, { resourceId: args.id, id: user.id }) + const resourceTypes = ['Post', 'Comment'] + const notifications = await result.records.map(record => { + return { + ...record.get('notification').properties, + from: { + __typename: record.get('resource').labels.find(l => resourceTypes.includes(l)), + ...record.get('resource').properties, + }, + to: { + __typename: 'User', + ...record.get('user').properties, + }, + } + }) + notification = notifications[0] + } finally { + session.close() + } + return notification }, }, } diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 72a9a8355..e11c9a73e 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,11 +1,9 @@ -import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login, gql } from '../../jest/helpers' +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 neode = getNeode() const driver = getDriver() @@ -16,6 +14,7 @@ const userParams = { } let authenticatedUser +let user let variables let query let mutate @@ -42,90 +41,82 @@ afterEach(async () => { await factory.cleanDatabase() }) -describe('notifications', () => { - const notificationQuery = gql` - query($read: Boolean, $orderBy: NOTIFIEDOrdering) { - notifications(read: $read, orderBy: $orderBy) { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` - +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-31T17: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); + `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('unauthenticated', () => { - it('throws authorization error', async () => { - const result = await query({ query: notificationQuery }) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') - }) - }) - - describe('authenticated', () => { - beforeEach(async () => { - const user = await factory.create('User', userParams) - authenticatedUser = await user.toJson() + 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 + } + } + ` + describe('unauthenticated', () => { + it('throws authorization error', async () => { + const result = await query({ query: notificationQuery }) + expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + }) }) - describe('given some notifications', () => { + describe('authenticated', () => { beforeEach(async () => { - await factory.create('User', { id: 'neighbor' }) - await Promise.all(setupNotifications.map(s => neode.cypher(s))) + authenticatedUser = await user.toJson() }) describe('no filters', () => { @@ -155,7 +146,7 @@ MERGE (comment) content: 'You have been mentioned in a comment', }, read: false, - createdAt: '2019-08-31T17:33:48.651Z', + createdAt: '2019-08-30T19:33:48.651Z', }, { from: { @@ -173,179 +164,130 @@ MERGE (comment) }) describe('filter for read: false', () => { - const queryCurrentUserNotificationsFilterRead = gql` - query($read: Boolean) { - notifications(read: $read, orderBy: createdAt_desc) { - id - post { - id - } - comment { - id - } - } - } - ` - it('returns only unread notifications of current user', async () => { - const expected = { - currentUser: { - notifications: expect.arrayContaining([ + const expected = expect.objectContaining({ + data: { + notifications: [ { - id: 'post-mention-unseen', - post: { - id: 'p1', + from: { + __typename: 'Comment', + content: 'You have been mentioned in a comment', }, - comment: null, + read: false, + createdAt: '2019-08-30T19:33:48.651Z', }, { - id: 'comment-mention-unseen', - post: null, - comment: { - id: 'c1', + from: { + __typename: 'Post', + content: 'You have been mentioned in a post', }, + read: false, + createdAt: '2019-08-31T17:33:48.651Z', }, - ]), + ], }, - } + }) await expect( - client.request(queryCurrentUserNotificationsFilterRead, variables), + query({ query: notificationQuery, variables: { ...variables, read: false } }), ).resolves.toEqual(expected) }) }) }) }) -}) -describe('UpdateNotification', () => { - const mutationUpdateNotification = gql` - mutation($id: ID!, $read: Boolean) { - UpdateNotification(id: $id, read: $read) { - id - read + describe('markAsRead', () => { + const markAsReadMutation = gql` + mutation($id: ID!) { + markAsRead(id: $id) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } } - } - ` - 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 mutate({ + mutation: markAsReadMutation, + variables: { ...variables, id: 'p1' }, + }) + 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() }) - it('throws authorization error', async () => { - await expect( - client.request(mutationUpdateNotification, variablesPostUpdateNotification), - ).rejects.toThrow('Not Authorised') - }) - - describe('and owner', () => { + describe('not being notified at all', () => { beforeEach(async () => { - headers = await login({ - email: 'mentioned@example.org', - password: '1234', + 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', + } }) - 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', + }, + }) }) }) - it('updates post notification', async () => { - const expected = { - UpdateNotification: { - id: 'post-mention-to-be-updated', - read: true, - }, - } - await expect( - client.request(mutationUpdateNotification, variablesPostUpdateNotification), - ).resolves.toEqual(expected) - }) + describe('on a comment', () => { + beforeEach(async () => { + variables = { + ...variables, + id: 'c2', + } + }) - it('updates comment notification', async () => { - const expected = { - UpdateNotification: { - id: 'comment-mention-to-be-updated', - read: true, - }, - } - await expect( - client.request(mutationUpdateNotification, variablesCommentUpdateNotification), - ).resolves.toEqual(expected) + 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', + }, + }) + }) }) }) }) diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/schema/types/type/NOTIFIED.gql index 0be774f30..965ee56d6 100644 --- a/backend/src/schema/types/type/NOTIFIED.gql +++ b/backend/src/schema/types/type/NOTIFIED.gql @@ -3,17 +3,24 @@ type NOTIFIED { to: User createdAt: String read: Boolean + reason: NotificationReason } union NotificationSource = Post | Comment -enum NOTIFIEDOrdering { +enum NotificationOrdering { createdAt_asc createdAt_desc } +enum NotificationReason { + mentioned_in_post + mentioned_in_comment + comment_on_post +} + type Query { - notifications(read: Boolean, orderBy: NOTIFIEDOrdering): [NOTIFIED] + notifications(read: Boolean, orderBy: NotificationOrdering): [NOTIFIED] } type Mutation { From 3f121c7c4df84729f1cf7ff78ec295d22ba8a382 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Thu, 29 Aug 2019 21:59:23 +0200 Subject: [PATCH 05/17] Refine notification behaviour: If you mark a post or comment as read even though you already marked the corresponding notification as read, then you receive `null`. --- backend/src/schema/resolvers/notifications.js | 2 +- backend/src/schema/resolvers/notifications.spec.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index e46b61ba4..2db62e444 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -62,7 +62,7 @@ export default { let notification try { const cypher = ` - MATCH (resource {id: $resourceId})-[notification:NOTIFIED]->(user:User {id:$id}) + MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id}) SET notification.read = TRUE RETURN resource, notification, user ` diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index e11c9a73e..b321d449f 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -265,6 +265,20 @@ describe('given some notifications', () => { }, }) }) + + 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', () => { From 733c2d5ce1feb5181e994f2786d1a252d46189e2 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Thu, 29 Aug 2019 22:14:31 +0200 Subject: [PATCH 06/17] All backend tests pass --- .../notificationsMiddleware.spec.js | 131 ++++++++---------- 1 file changed, 54 insertions(+), 77 deletions(-) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.js b/backend/src/middleware/notifications/notificationsMiddleware.spec.js index 624cedddc..709ea7ba1 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.js @@ -77,14 +77,15 @@ 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 + from { + __typename + ... on Post { content } - comment { + ... on Comment { content } } @@ -154,18 +155,16 @@ 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, + reason: 'comment_on_post', + from: { + __typename: 'Comment', + content: commentContent, }, - ], - }, + }, + ], }, }) const { query } = createTestClient(server) @@ -183,11 +182,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 +206,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 +244,16 @@ describe('notifications', () => { 'Hey @al-capone 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, + reason: 'mentioned_in_post', + from: { + __typename: 'Post', + content: expectedContent, }, - ], - }, + }, + ], }, }) const { query } = createTestClient(server) @@ -314,26 +303,24 @@ describe('notifications', () => { '
One more mention to

@al-capone

and again:

@al-capone

and again

@al-capone

' const expected = expect.objectContaining({ data: { - currentUser: { - notifications: [ - { - read: false, - reason: 'mentioned_in_post', - post: { - content: expectedContent, - }, - comment: null, + notifications: [ + { + read: false, + reason: 'mentioned_in_post', + from: { + __typename: 'Post', + content: expectedContent, }, - { - read: false, - reason: 'mentioned_in_post', - post: { - content: expectedContent, - }, - comment: null, + }, + { + read: false, + reason: 'mentioned_in_post', + from: { + __typename: 'Post', + content: expectedContent, }, - ], - }, + }, + ], }, }) await expect( @@ -355,11 +342,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 +380,16 @@ 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, + reason: 'mentioned_in_comment', + from: { + __typename: 'Comment', + content: commentContent, }, - ], - }, + }, + ], }, }) const { query } = createTestClient(server) @@ -440,11 +421,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( From 9b2c707aa3da1bea20aece372e8dfa8774efa7ce Mon Sep 17 00:00:00 2001 From: roschaefer Date: Thu, 29 Aug 2019 22:22:48 +0200 Subject: [PATCH 07/17] DRY notifications resolver --- backend/src/schema/resolvers/notifications.js | 45 +++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index 2db62e444..2385c4b1a 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -1,3 +1,18 @@ +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: { notifications: async (parent, args, context, resolveInfo) => { @@ -35,20 +50,7 @@ export default { ${orderByClause} ` const result = await session.run(cypher, { id: user.id }) - const resourceTypes = ['Post', 'Comment'] - notifications = await result.records.map(record => { - return { - ...record.get('notification').properties, - from: { - __typename: record.get('resource').labels.find(l => resourceTypes.includes(l)), - ...record.get('resource').properties, - }, - to: { - __typename: 'User', - ...record.get('user').properties, - }, - } - }) + notifications = await result.records.map(transformReturnType) } finally { session.close() } @@ -67,20 +69,7 @@ export default { RETURN resource, notification, user ` const result = await session.run(cypher, { resourceId: args.id, id: user.id }) - const resourceTypes = ['Post', 'Comment'] - const notifications = await result.records.map(record => { - return { - ...record.get('notification').properties, - from: { - __typename: record.get('resource').labels.find(l => resourceTypes.includes(l)), - ...record.get('resource').properties, - }, - to: { - __typename: 'User', - ...record.get('user').properties, - }, - } - }) + const notifications = await result.records.map(transformReturnType) notification = notifications[0] } finally { session.close() From bf6e7a8131c2453b236531914f8206982a11b98a Mon Sep 17 00:00:00 2001 From: roschaefer Date: Thu, 29 Aug 2019 22:55:49 +0200 Subject: [PATCH 08/17] Implement fallback resolvers for Post and Comment --- backend/src/schema/resolvers/comments.js | 10 ++++++ backend/src/schema/resolvers/posts.js | 43 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/backend/src/schema/resolvers/comments.js b/backend/src/schema/resolvers/comments.js index 89a2040f4..c8fb8db3f 100644 --- a/backend/src/schema/resolvers/comments.js +++ b/backend/src/schema/resolvers/comments.js @@ -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)', + }, + }), + }, } diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index a265c28f0..f9e4f0718 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -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 @@ -181,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 + }, + }, } From c585b23d7a587b43ad182c46f5222529b10240d1 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Fri, 30 Aug 2019 01:24:15 +0200 Subject: [PATCH 09/17] Fix notification queries in webapp --- .../Notification/Notification.vue | 35 ++--- .../NotificationList/NotificationList.vue | 6 +- .../NotificationMenu/NotificationMenu.vue | 39 ++--- webapp/graphql/User.js | 135 ++++++++++-------- webapp/plugins/apollo-config.js | 8 ++ .../plugins/apollo-config/fragmentTypes.json | 18 +++ webapp/store/auth.js | 17 --- webapp/store/notifications.js | 99 ------------- 8 files changed, 138 insertions(+), 219 deletions(-) create mode 100644 webapp/plugins/apollo-config/fragmentTypes.json delete mode 100644 webapp/store/notifications.js diff --git a/webapp/components/notifications/Notification/Notification.vue b/webapp/components/notifications/Notification/Notification.vue index 193b5f67b..31b61be40 100644 --- a/webapp/components/notifications/Notification/Notification.vue +++ b/webapp/components/notifications/Notification/Notification.vue @@ -2,13 +2,7 @@ - - + {{ $t(`notifications.menu.${notification.reason}`) }} @@ -22,16 +16,15 @@ > -
{{ post.contentExcerpt | removeHtml }}
-
- Comment: - {{ comment.contentExcerpt | removeHtml }} +
+ Comment: + {{ from.contentExcerpt | removeHtml }}
@@ -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}` } : {} }, }, } diff --git a/webapp/components/notifications/NotificationList/NotificationList.vue b/webapp/components/notifications/NotificationList/NotificationList.vue index 43783ea56..aac040785 100644 --- a/webapp/components/notifications/NotificationList/NotificationList.vue +++ b/webapp/components/notifications/NotificationList/NotificationList.vue @@ -4,7 +4,7 @@ v-for="notification in notifications" :key="notification.id" :notification="notification" - @read="markAsRead(notification.id)" + @read="markAsRead(notification.from.id)" />
@@ -24,8 +24,8 @@ export default { }, }, methods: { - markAsRead(notificationId) { - this.$emit('markAsRead', notificationId) + async markAsRead(notificationSourceId) { + this.$emit('markAsRead', notificationSourceId) }, }, } diff --git a/webapp/components/notifications/NotificationMenu/NotificationMenu.vue b/webapp/components/notifications/NotificationMenu/NotificationMenu.vue index c534f2986..8dc496efc 100644 --- a/webapp/components/notifications/NotificationMenu/NotificationMenu.vue +++ b/webapp/components/notifications/NotificationMenu/NotificationMenu.vue @@ -18,7 +18,7 @@