From 7bbdd5852f5b889abc3a668903ff95bde1d11669 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 15 Feb 2021 17:00:31 +0100 Subject: [PATCH 01/10] count clicks on posts setup --- backend/src/middleware/index.js | 3 +++ backend/src/middleware/userInteractions.js | 14 ++++++++++++++ backend/src/schema/resolvers/posts.js | 1 + backend/src/schema/types/type/Post.gql | 4 ++++ 4 files changed, 22 insertions(+) create mode 100644 backend/src/middleware/userInteractions.js diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 0ad8cb1ae..592e25a60 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -15,6 +15,7 @@ import hashtags from './hashtags/hashtagsMiddleware' import email from './email/emailMiddleware' import sentry from './sentryMiddleware' import languages from './languages/languages' +import userInteractions from './userInteractions' export default (schema) => { const middlewares = { @@ -32,6 +33,7 @@ export default (schema) => { includedFields, orderBy, languages, + userInteractions, } let order = [ @@ -40,6 +42,7 @@ export default (schema) => { 'xss', // 'activityPub', disabled temporarily 'validation', + 'userInteractions', 'sluggify', 'languages', 'excerpt', diff --git a/backend/src/middleware/userInteractions.js b/backend/src/middleware/userInteractions.js new file mode 100644 index 000000000..8455d05dd --- /dev/null +++ b/backend/src/middleware/userInteractions.js @@ -0,0 +1,14 @@ + + +const userClickedPost = async (resolve, root, args, context, info) => { + if (args.id) { + console.log('post clicked--', args.id) + } + return resolve(root, args, context, info) +} + +export default { + Query: { + Post: userClickedPost, + }, +} diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 14e645730..a02eb599d 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -337,6 +337,7 @@ export default { shoutedCount: '<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true', emotionsCount: '<-[related:EMOTED]-(:User)', + clickedCount: '<-[related:CLICKED]-(:User)', }, boolean: { shoutedByCurrentUser: diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 37f9dd176..f7f5aa69f 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -156,6 +156,10 @@ type Post { statement: "MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1" ) + clickedCount: Int! + @cypher( + statement: "MATCH (this)<-[:CLICKED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)" + ) emotions: [EMOTED] emotionsCount: Int! @cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)") From 7c5e5d971e0f0d4c70fb10be041515a4616dde83 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 22 Feb 2021 19:07:44 +0100 Subject: [PATCH 02/10] cypher to track user interactions --- backend/src/middleware/userInteractions.js | 32 +++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/backend/src/middleware/userInteractions.js b/backend/src/middleware/userInteractions.js index 8455d05dd..91dc7f80f 100644 --- a/backend/src/middleware/userInteractions.js +++ b/backend/src/middleware/userInteractions.js @@ -1,8 +1,38 @@ +const createRelatedCypher = (relation) => ` +MATCH (user:User { id: $currentUser}) +MATCH (post:Post { id: $postId})<-[r:${relation}]-(u:User) +WHERE NOT u.disabled AND NOT u.deleted +WITH user, post, count(DISTINCT u) AS count +MERGE (user)-[relation:${relation} { }]->(post) +ON CREATE +SET relation.count = 1, +relation.createdAt = toString(datetime()), +post.clickCount = count + 1 +ON MATCH +SET relation.count = relation.count + 1, +relation.updatedAt = toString(datetime()), +post.clickCount = count +RETURN user, post, relation +` +const setPostCounter = async (postId, relation, context) => { + const { user: currentUser } = context + const session = context.driver.session() + try { + await session.writeTransaction((txc) => { + return txc.run( + createRelatedCypher(relation), + { currentUser, postId }, + ) + }) + } finally { + session.close() + } +} const userClickedPost = async (resolve, root, args, context, info) => { if (args.id) { - console.log('post clicked--', args.id) + await setPostCounter(args.id, 'CLICKED', context) } return resolve(root, args, context, info) } From 4862d53196081b71b71a6150905bbaef1ae05aa1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 22 Feb 2021 20:49:33 +0100 Subject: [PATCH 03/10] tests added for click count --- backend/src/db/factories.js | 1 + backend/src/middleware/userInteractions.js | 18 ++-- .../src/middleware/userInteractions.spec.js | 98 +++++++++++++++++++ backend/src/models/Post.js | 1 + backend/src/schema/resolvers/posts.js | 1 + backend/src/schema/types/type/Post.gql | 4 +- 6 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 backend/src/middleware/userInteractions.spec.js diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index 8a6ca380e..1f636d981 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -130,6 +130,7 @@ Factory.define('post') deleted: false, imageBlurred: false, imageAspectRatio: 1.333, + clickedCount: 0, }) .attr('pinned', ['pinned'], (pinned) => { // Convert false to null diff --git a/backend/src/middleware/userInteractions.js b/backend/src/middleware/userInteractions.js index 91dc7f80f..553aefe78 100644 --- a/backend/src/middleware/userInteractions.js +++ b/backend/src/middleware/userInteractions.js @@ -1,33 +1,33 @@ const createRelatedCypher = (relation) => ` MATCH (user:User { id: $currentUser}) -MATCH (post:Post { id: $postId})<-[r:${relation}]-(u:User) +MATCH (post:Post { id: $postId}) +OPTIONAL MATCH (post)<-[r:${relation}]-(u:User) WHERE NOT u.disabled AND NOT u.deleted WITH user, post, count(DISTINCT u) AS count MERGE (user)-[relation:${relation} { }]->(post) ON CREATE SET relation.count = 1, relation.createdAt = toString(datetime()), -post.clickCount = count + 1 +post.clickedCount = count + 1 ON MATCH SET relation.count = relation.count + 1, relation.updatedAt = toString(datetime()), -post.clickCount = count +post.clickedCount = count RETURN user, post, relation ` const setPostCounter = async (postId, relation, context) => { - const { user: currentUser } = context + const { + user: { id: currentUser }, + } = context const session = context.driver.session() try { await session.writeTransaction((txc) => { - return txc.run( - createRelatedCypher(relation), - { currentUser, postId }, - ) + return txc.run(createRelatedCypher(relation), { currentUser, postId }) }) } finally { session.close() - } + } } const userClickedPost = async (resolve, root, args, context, info) => { diff --git a/backend/src/middleware/userInteractions.spec.js b/backend/src/middleware/userInteractions.spec.js new file mode 100644 index 000000000..77c9fbd1d --- /dev/null +++ b/backend/src/middleware/userInteractions.spec.js @@ -0,0 +1,98 @@ +import Factory, { cleanDatabase } from '../db/factories' +import { gql } from '../helpers/jest' +import { getNeode, getDriver } from '../db/neo4j' +import createServer from '../server' +import { createTestClient } from 'apollo-server-testing' + +let query, aUser, bUser, post, authenticatedUser, variables + +const driver = getDriver() +const neode = getNeode() + +const postQuery = gql` + query($id: ID) { + Post(id: $id) { + clickedCount + } + } +` + +beforeAll(async () => { + await cleanDatabase() + aUser = await Factory.build('user', { + id: 'a-user', + }) + bUser = await Factory.build('user', { + id: 'b-user', + }) + post = await Factory.build('post') + authenticatedUser = await aUser.toJson() + const { server } = createServer({ + context: () => { + return { + driver, + neode, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query +}) + +afterAll(async () => { + await cleanDatabase() +}) + +describe('middleware/userInteractions', () => { + describe('given one post', () => { + it('does not change clickedCount when queried without ID', async () => { + await expect(query({ query: postQuery, variables })).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + clickedCount: 0, + }, + ]), + }, + }) + }) + + it('changes clickedCount when queried with ID', async () => { + variables = { id: post.get('id') } + await expect(query({ query: postQuery, variables })).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + clickedCount: 1, + }, + ]), + }, + }) + }) + + it('does not change clickedCount when same user queries the post again', async () => { + await expect(query({ query: postQuery, variables })).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + clickedCount: 1, + }, + ]), + }, + }) + }) + + it('changes clickedCount when another user queries the post', async () => { + authenticatedUser = await bUser.toJson() + await expect(query({ query: postQuery, variables })).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + clickedCount: 2, + }, + ]), + }, + }) + }) + }) +}) diff --git a/backend/src/models/Post.js b/backend/src/models/Post.js index 43f63ebd3..42bf844e1 100644 --- a/backend/src/models/Post.js +++ b/backend/src/models/Post.js @@ -22,6 +22,7 @@ export default { contentExcerpt: { type: 'string', allow: [null] }, deleted: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false }, + clickedCount: { type: 'int', default: 0 }, notified: { type: 'relationship', relationship: 'NOTIFIED', diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index a02eb599d..d0452bc78 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -88,6 +88,7 @@ export default { SET post += $params SET post.createdAt = toString(datetime()) SET post.updatedAt = toString(datetime()) + SET post.clickedCount = 0 WITH post MATCH (author:User {id: $userId}) MERGE (post)<-[:WROTE]-(author) diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index f7f5aa69f..99e1e9e59 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -157,9 +157,7 @@ type Post { ) clickedCount: Int! - @cypher( - statement: "MATCH (this)<-[:CLICKED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)" - ) + emotions: [EMOTED] emotionsCount: Int! @cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)") From 4611e298f58ae86c45e75b61ba3330d152171f9d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 22 Feb 2021 21:03:12 +0100 Subject: [PATCH 04/10] migration for clickedCount --- ...1614023644903-add-clickedCount-to-posts.js | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.js diff --git a/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.js b/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.js new file mode 100644 index 000000000..24d2ded0e --- /dev/null +++ b/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.js @@ -0,0 +1,53 @@ +import { getDriver } from '../../db/neo4j' + +export const description = ` +This migration adds the clickedCount property to all posts, setting it to 0. +` + +module.exports.up = async function (next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + try { + // Implement your migration here. + await transaction.run(` + MATCH (p:Post) + SET p.clickedCount = 0 + `) + await transaction.commit() + next() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + session.close() + } +} + +module.exports.down = async function (next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + try { + // Implement your migration here. + await transaction.run(` + MATCH (p:Post) + REMOVE p.clickedCount + `) + await transaction.commit() + next() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + session.close() + } +} From d3eafc9b69c70e31021c6e2723bd5bcfa6b9d17f Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 22 Feb 2021 21:13:24 +0100 Subject: [PATCH 05/10] add clickedCount to PostTeaser --- webapp/components/PostTeaser/PostTeaser.vue | 263 ++++++++++---------- webapp/graphql/Fragments.js | 1 + webapp/locales/de.json | 1 + webapp/locales/en.json | 1 + 4 files changed, 137 insertions(+), 129 deletions(-) diff --git a/webapp/components/PostTeaser/PostTeaser.vue b/webapp/components/PostTeaser/PostTeaser.vue index 6b768c365..bd9628452 100644 --- a/webapp/components/PostTeaser/PostTeaser.vue +++ b/webapp/components/PostTeaser/PostTeaser.vue @@ -6,9 +6,9 @@