diff --git a/backend/src/middleware/handleNotifications/hashtags/extractHashtags.js b/backend/src/middleware/hashtags/extractHashtags.js similarity index 100% rename from backend/src/middleware/handleNotifications/hashtags/extractHashtags.js rename to backend/src/middleware/hashtags/extractHashtags.js diff --git a/backend/src/middleware/handleNotifications/hashtags/extractHashtags.spec.js b/backend/src/middleware/hashtags/extractHashtags.spec.js similarity index 100% rename from backend/src/middleware/handleNotifications/hashtags/extractHashtags.spec.js rename to backend/src/middleware/hashtags/extractHashtags.spec.js diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.js b/backend/src/middleware/hashtags/hashtagsMiddleware.js new file mode 100644 index 000000000..c9156398d --- /dev/null +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.js @@ -0,0 +1,49 @@ +import extractHashtags from '../hashtags/extractHashtags' + +const updateHashtagsOfPost = async (postId, hashtags, context) => { + if (!hashtags.length) return + + const session = context.driver.session() + // We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement + // functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted + // and no new Hashtags and relations will be created. + const cypherDeletePreviousRelations = ` + MATCH (p: Post { id: $postId })-[previousRelations: TAGGED]->(t: Tag) + DELETE previousRelations + RETURN p, t + ` + const cypherCreateNewTagsAndRelations = ` + MATCH (p: Post { id: $postId}) + UNWIND $hashtags AS tagName + MERGE (t: Tag { id: tagName, disabled: false, deleted: false }) + MERGE (p)-[:TAGGED]->(t) + RETURN p, t + ` + await session.run(cypherDeletePreviousRelations, { + postId, + }) + await session.run(cypherCreateNewTagsAndRelations, { + postId, + hashtags, + }) + session.close() +} + +const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { + const hashtags = extractHashtags(args.content) + + const post = await resolve(root, args, context, resolveInfo) + + if (post) { + await updateHashtagsOfPost(post.id, hashtags, context) + } + + return post +} + +export default { + Mutation: { + CreatePost: handleContentDataOfPost, + UpdatePost: handleContentDataOfPost, + }, +} diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js new file mode 100644 index 000000000..3f101f778 --- /dev/null +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js @@ -0,0 +1,176 @@ +import { gql } from '../../jest/helpers' +import Factory from '../../seed/factories' +import { createTestClient } from 'apollo-server-testing' +import { neode, getDriver } from '../../bootstrap/neo4j' +import createServer from '../../server' + +let server +let query +let mutate +let hashtagingUser +let authenticatedUser +const factory = Factory() +const driver = getDriver() +const instance = neode() +const categoryIds = ['cat9'] +const createPostMutation = gql` + mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) { + CreatePost(id: $id, title: $title, content: $postContent, categoryIds: $categoryIds) { + id + title + content + } + } +` +const updatePostMutation = gql` + mutation($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) { + UpdatePost(id: $id, content: $postContent, title: $title, categoryIds: $categoryIds) { + title + content + } + } +` + +beforeAll(() => { + const createServerResult = createServer({ + context: () => { + return { + user: authenticatedUser, + neode: instance, + driver, + } + }, + }) + server = createServerResult.server + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +beforeEach(async () => { + hashtagingUser = await instance.create('User', { + id: 'you', + name: 'Al Capone', + slug: 'al-capone', + email: 'test@example.org', + password: '1234', + }) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('hashtags', () => { + const id = 'p135' + const title = 'Two Hashtags' + const postContent = + '

Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' + const postWithHastagsQuery = gql` + query($id: ID) { + Post(id: $id) { + tags { + id + } + } + } + ` + const postWithHastagsVariables = { + id, + } + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await hashtagingUser.toJson() + }) + + describe('create a Post with Hashtags', () => { + beforeEach(async () => { + await mutate({ + mutation: createPostMutation, + variables: { + id, + title, + postContent, + categoryIds, + }, + }) + }) + + it('both hashtags are created with the "id" set to their "name"', async () => { + const expected = [ + { + id: 'Democracy', + }, + { + id: 'Liberty', + }, + ] + await expect( + query({ + query: postWithHastagsQuery, + variables: postWithHastagsVariables, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + Post: [ + { + tags: expect.arrayContaining(expected), + }, + ], + }, + }), + ) + }) + + describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => { + // The already existing Hashtag has no class at this point. + const postContent = + '

Hey Dude, #Elections should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' + + it('only one previous Hashtag and the new Hashtag exists', async () => { + await mutate({ + mutation: updatePostMutation, + variables: { + id, + title, + postContent, + categoryIds, + }, + }) + + const expected = [ + { + id: 'Elections', + }, + { + id: 'Liberty', + }, + ] + await expect( + query({ + query: postWithHastagsQuery, + variables: postWithHastagsVariables, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + Post: [ + { + tags: expect.arrayContaining(expected), + }, + ], + }, + }), + ) + }) + }) + }) + }) +}) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 7b81204df..7774ccc15 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -12,7 +12,8 @@ import user from './userMiddleware' import includedFields from './includedFieldsMiddleware' import orderBy from './orderByMiddleware' import validation from './validation/validationMiddleware' -import handleNotifications from './handleNotifications/handleNotificationsMiddleware' +import notifications from './notifications/notificationsMiddleware' +import hashtags from './hashtags/hashtagsMiddleware' import email from './email/emailMiddleware' import sentry from './sentryMiddleware' @@ -25,13 +26,16 @@ export default schema => { validation, sluggify, excerpt, - handleNotifications, + notifications, + hashtags, xss, softDelete, user, includedFields, orderBy, - email: email({ isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT }), + email: email({ + isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT, + }), } let order = [ @@ -43,7 +47,8 @@ export default schema => { 'sluggify', 'excerpt', 'email', - 'handleNotifications', + 'notifications', + 'hashtags', 'xss', 'softDelete', 'user', diff --git a/backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.js b/backend/src/middleware/notifications/mentions/extractMentionedUsers.js similarity index 100% rename from backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.js rename to backend/src/middleware/notifications/mentions/extractMentionedUsers.js diff --git a/backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.spec.js b/backend/src/middleware/notifications/mentions/extractMentionedUsers.spec.js similarity index 100% rename from backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.spec.js rename to backend/src/middleware/notifications/mentions/extractMentionedUsers.spec.js diff --git a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js similarity index 76% rename from backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js rename to backend/src/middleware/notifications/notificationsMiddleware.js index dcd1be1d9..c9dfe406c 100644 --- a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -1,5 +1,4 @@ -import extractMentionedUsers from './notifications/extractMentionedUsers' -import extractHashtags from './hashtags/extractHashtags' +import extractMentionedUsers from './mentions/extractMentionedUsers' const notifyUsers = async (label, id, idsOfUsers, reason, context) => { if (!idsOfUsers.length) return @@ -66,44 +65,13 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { session.close() } -const updateHashtagsOfPost = async (postId, hashtags, context) => { - if (!hashtags.length) return - - const session = context.driver.session() - // We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement - // functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted - // and no new Hashtags and relations will be created. - const cypherDeletePreviousRelations = ` - MATCH (p: Post { id: $postId })-[previousRelations: TAGGED]->(t: Tag) - DELETE previousRelations - RETURN p, t - ` - const cypherCreateNewTagsAndRelations = ` - MATCH (p: Post { id: $postId}) - UNWIND $hashtags AS tagName - MERGE (t: Tag { id: tagName, disabled: false, deleted: false }) - MERGE (p)-[:TAGGED]->(t) - RETURN p, t - ` - await session.run(cypherDeletePreviousRelations, { - postId, - }) - await session.run(cypherCreateNewTagsAndRelations, { - postId, - hashtags, - }) - session.close() -} - const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { const idsOfUsers = extractMentionedUsers(args.content) - const hashtags = extractHashtags(args.content) const post = await resolve(root, args, context, resolveInfo) if (post) { await notifyUsers('Post', post.id, idsOfUsers, 'mentioned_in_post', context) - await updateHashtagsOfPost(post.id, hashtags, context) } return post @@ -121,7 +89,6 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI } const handleCreateComment = async (resolve, root, args, context, resolveInfo) => { - // removes classes from the content const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo) if (comment) { diff --git a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js b/backend/src/middleware/notifications/notificationsMiddleware.spec.js similarity index 82% rename from backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js rename to backend/src/middleware/notifications/notificationsMiddleware.spec.js index 3b52d13e0..624cedddc 100644 --- a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.js @@ -461,112 +461,3 @@ describe('notifications', () => { }) }) }) - -describe('Hashtags', () => { - const id = 'p135' - const title = 'Two Hashtags' - const postContent = - '

Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - const postWithHastagsQuery = gql` - query($id: ID) { - Post(id: $id) { - tags { - id - } - } - } - ` - const postWithHastagsVariables = { - id, - } - - describe('authenticated', () => { - beforeEach(async () => { - authenticatedUser = await notifiedUser.toJson() - }) - - describe('create a Post with Hashtags', () => { - beforeEach(async () => { - await mutate({ - mutation: createPostMutation, - variables: { - id, - title, - postContent, - categoryIds, - }, - }) - }) - - it('both Hashtags are created with the "id" set to their "name"', async () => { - const expected = [ - { - id: 'Democracy', - }, - { - id: 'Liberty', - }, - ] - await expect( - query({ - query: postWithHastagsQuery, - variables: postWithHastagsVariables, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - Post: [ - { - tags: expect.arrayContaining(expected), - }, - ], - }, - }), - ) - }) - - describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => { - // The already existing Hashtag has no class at this point. - const postContent = - '

Hey Dude, #Elections should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - - it('only one previous Hashtag and the new Hashtag exists', async () => { - await mutate({ - mutation: updatePostMutation, - variables: { - id, - title, - postContent, - categoryIds, - }, - }) - - const expected = [ - { - id: 'Elections', - }, - { - id: 'Liberty', - }, - ] - await expect( - query({ - query: postWithHastagsQuery, - variables: postWithHastagsVariables, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - Post: [ - { - tags: expect.arrayContaining(expected), - }, - ], - }, - }), - ) - }) - }) - }) - }) -})