mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
feat(backend): observe posts (#8292)
* After creating the post, the author of it automatically observes it to get notifications when there are interactions * a user that comments a post, automatically observes that post to get notifications when there are more interactions on that post * mutation that switches the state of the observation of a post on and off
This commit is contained in:
parent
54c2e5c131
commit
538f409086
@ -46,6 +46,8 @@ export const createPostMutation = () => {
|
|||||||
lng
|
lng
|
||||||
lat
|
lat
|
||||||
}
|
}
|
||||||
|
isObservedByMe
|
||||||
|
observingUsersCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -465,6 +465,7 @@ export default shield(
|
|||||||
CreateRoom: isAuthenticated,
|
CreateRoom: isAuthenticated,
|
||||||
CreateMessage: isAuthenticated,
|
CreateMessage: isAuthenticated,
|
||||||
MarkMessagesAsSeen: isAuthenticated,
|
MarkMessagesAsSeen: isAuthenticated,
|
||||||
|
toggleObservePost: isAuthenticated,
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: or(isMyOwn, isAdmin),
|
email: or(isMyOwn, isAdmin),
|
||||||
|
|||||||
@ -21,6 +21,9 @@ const { server } = createServer({
|
|||||||
driver,
|
driver,
|
||||||
neode,
|
neode,
|
||||||
user: authenticatedUser,
|
user: authenticatedUser,
|
||||||
|
cypherParams: {
|
||||||
|
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -25,6 +25,12 @@ export default {
|
|||||||
SET comment.createdAt = toString(datetime())
|
SET comment.createdAt = toString(datetime())
|
||||||
SET comment.updatedAt = toString(datetime())
|
SET comment.updatedAt = toString(datetime())
|
||||||
MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
|
MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
|
||||||
|
WITH post, author, comment
|
||||||
|
MERGE (post)<-[obs:OBSERVES]-(author)
|
||||||
|
ON CREATE SET
|
||||||
|
obs.active = true,
|
||||||
|
obs.createdAt = toString(datetime()),
|
||||||
|
obs.updatedAt = toString(datetime())
|
||||||
RETURN comment
|
RETURN comment
|
||||||
`,
|
`,
|
||||||
{ userId: user.id, postId, params },
|
{ userId: user.id, postId, params },
|
||||||
|
|||||||
240
backend/src/schema/resolvers/observePosts.spec.ts
Normal file
240
backend/src/schema/resolvers/observePosts.spec.ts
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
|
import createServer from '../../server'
|
||||||
|
|
||||||
|
import { createPostMutation } from '../../graphql/posts'
|
||||||
|
import CONFIG from '../../config'
|
||||||
|
|
||||||
|
CONFIG.CATEGORIES_ACTIVE = false
|
||||||
|
|
||||||
|
const driver = getDriver()
|
||||||
|
const neode = getNeode()
|
||||||
|
|
||||||
|
let query
|
||||||
|
let mutate
|
||||||
|
let authenticatedUser
|
||||||
|
let user
|
||||||
|
let otherUser
|
||||||
|
|
||||||
|
const createCommentMutation = gql`
|
||||||
|
mutation ($id: ID, $postId: ID!, $content: String!) {
|
||||||
|
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const postQuery = gql`
|
||||||
|
query Post($id: ID) {
|
||||||
|
Post(id: $id) {
|
||||||
|
isObservedByMe
|
||||||
|
observingUsersCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await cleanDatabase()
|
||||||
|
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
driver,
|
||||||
|
neode,
|
||||||
|
user: authenticatedUser,
|
||||||
|
cypherParams: {
|
||||||
|
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
query = createTestClient(server).query
|
||||||
|
mutate = createTestClient(server).mutate
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDatabase()
|
||||||
|
driver.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('observing posts', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
user = await Factory.build('user', {
|
||||||
|
id: 'user',
|
||||||
|
name: 'User',
|
||||||
|
about: 'I am a user',
|
||||||
|
})
|
||||||
|
otherUser = await Factory.build('user', {
|
||||||
|
id: 'other-user',
|
||||||
|
name: 'Other User',
|
||||||
|
about: 'I am another user',
|
||||||
|
})
|
||||||
|
authenticatedUser = await user.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('creating posts', () => {
|
||||||
|
it('has the author of the post observing the post', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: createPostMutation(),
|
||||||
|
variables: {
|
||||||
|
id: 'p2',
|
||||||
|
title: 'A post the author should observe',
|
||||||
|
content: 'The author of this post is expected to observe the post',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
CreatePost: {
|
||||||
|
isObservedByMe: true,
|
||||||
|
observingUsersCount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('commenting posts', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
authenticatedUser = await otherUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has another user NOT observing the post BEFORE commenting it', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: postQuery,
|
||||||
|
variables: { id: 'p2' },
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
Post: [
|
||||||
|
{
|
||||||
|
isObservedByMe: false,
|
||||||
|
observingUsersCount: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has another user observing the post AFTER commenting it', async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: createCommentMutation,
|
||||||
|
variables: {
|
||||||
|
postId: 'p2',
|
||||||
|
content: 'After commenting the post, I should observe the post automatically',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: postQuery,
|
||||||
|
variables: { id: 'p2' },
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
Post: [
|
||||||
|
{
|
||||||
|
isObservedByMe: true,
|
||||||
|
observingUsersCount: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('toggle observe post', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
authenticatedUser = await otherUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleObservePostMutation = gql`
|
||||||
|
mutation ($id: ID!, $value: Boolean!) {
|
||||||
|
toggleObservePost(id: $id, value: $value) {
|
||||||
|
isObservedByMe
|
||||||
|
observingUsersCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
describe('switch off observation', () => {
|
||||||
|
it('does not observe the post anymore', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: toggleObservePostMutation,
|
||||||
|
variables: {
|
||||||
|
id: 'p2',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
toggleObservePost: {
|
||||||
|
isObservedByMe: false,
|
||||||
|
observingUsersCount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('comment the post again', () => {
|
||||||
|
it('does NOT alter the observation state', async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: createCommentMutation,
|
||||||
|
variables: {
|
||||||
|
postId: 'p2',
|
||||||
|
content:
|
||||||
|
'After commenting the post I do not observe again, I should NOT observe the post',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: postQuery,
|
||||||
|
variables: { id: 'p2' },
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
Post: [
|
||||||
|
{
|
||||||
|
isObservedByMe: false,
|
||||||
|
observingUsersCount: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('switch on observation', () => {
|
||||||
|
it('does observe the post again', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: toggleObservePostMutation,
|
||||||
|
variables: {
|
||||||
|
id: 'p2',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
toggleObservePost: {
|
||||||
|
isObservedByMe: true,
|
||||||
|
observingUsersCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -28,6 +28,9 @@ beforeAll(async () => {
|
|||||||
driver,
|
driver,
|
||||||
neode,
|
neode,
|
||||||
user: authenticatedUser,
|
user: authenticatedUser,
|
||||||
|
cypherParams: {
|
||||||
|
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -144,6 +144,10 @@ export default {
|
|||||||
WITH post
|
WITH post
|
||||||
MATCH (author:User {id: $userId})
|
MATCH (author:User {id: $userId})
|
||||||
MERGE (post)<-[:WROTE]-(author)
|
MERGE (post)<-[:WROTE]-(author)
|
||||||
|
MERGE (post)<-[obs:OBSERVES]-(author)
|
||||||
|
SET obs.active = true
|
||||||
|
SET obs.createdAt = toString(datetime())
|
||||||
|
SET obs.updatedAt = toString(datetime())
|
||||||
${categoriesCypher}
|
${categoriesCypher}
|
||||||
${groupCypher}
|
${groupCypher}
|
||||||
RETURN post {.*, postType: [l IN labels(post) WHERE NOT l = 'Post'] }
|
RETURN post {.*, postType: [l IN labels(post) WHERE NOT l = 'Post'] }
|
||||||
@ -416,6 +420,35 @@ export default {
|
|||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
toggleObservePost: async (_parent, params, context, _resolveInfo) => {
|
||||||
|
const session = context.driver.session()
|
||||||
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
|
const transactionResponse = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (post:Post { id: $params.id })
|
||||||
|
MATCH (user:User { id: $userId })
|
||||||
|
MERGE (user)-[obs:OBSERVES]->(post)
|
||||||
|
ON CREATE SET
|
||||||
|
obs.createdAt = toString(datetime()),
|
||||||
|
obs.updatedAt = toString(datetime()),
|
||||||
|
obs.active = $params.value
|
||||||
|
ON MATCH SET
|
||||||
|
obs.updatedAt = toString(datetime()),
|
||||||
|
obs.active = $params.value
|
||||||
|
RETURN post
|
||||||
|
`,
|
||||||
|
{ userId: context.user.id, params },
|
||||||
|
)
|
||||||
|
return transactionResponse.records.map((record) => record.get('post').properties)
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const [post] = await writeTxResultPromise
|
||||||
|
post.viewedTeaserCount = post.viewedTeaserCount.low
|
||||||
|
return post
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Post: {
|
Post: {
|
||||||
...Resolver('Post', {
|
...Resolver('Post', {
|
||||||
@ -452,12 +485,15 @@ export default {
|
|||||||
shoutedCount:
|
shoutedCount:
|
||||||
'<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true',
|
'<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true',
|
||||||
emotionsCount: '<-[related:EMOTED]-(:User)',
|
emotionsCount: '<-[related:EMOTED]-(:User)',
|
||||||
|
observingUsersCount: '<-[related:OBSERVES]-(:User) WHERE related.active = true',
|
||||||
},
|
},
|
||||||
boolean: {
|
boolean: {
|
||||||
shoutedByCurrentUser:
|
shoutedByCurrentUser:
|
||||||
'MATCH(this)<-[:SHOUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1',
|
'MATCH(this)<-[:SHOUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1',
|
||||||
viewedTeaserByCurrentUser:
|
viewedTeaserByCurrentUser:
|
||||||
'MATCH (this)<-[:VIEWED_TEASER]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
'MATCH (this)<-[:VIEWED_TEASER]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||||
|
isObservedByMe:
|
||||||
|
'MATCH (this)<-[obs:OBSERVES]-(related:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(related) >= 1',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
relatedContributions: async (parent, params, context, resolveInfo) => {
|
relatedContributions: async (parent, params, context, resolveInfo) => {
|
||||||
|
|||||||
@ -52,6 +52,9 @@ beforeAll(async () => {
|
|||||||
driver,
|
driver,
|
||||||
neode,
|
neode,
|
||||||
user: authenticatedUser,
|
user: authenticatedUser,
|
||||||
|
cypherParams: {
|
||||||
|
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -186,6 +186,13 @@ type Post {
|
|||||||
eventStart: String
|
eventStart: String
|
||||||
eventEnd: String
|
eventEnd: String
|
||||||
eventIsOnline: Boolean
|
eventIsOnline: Boolean
|
||||||
|
|
||||||
|
isObservedByMe: Boolean!
|
||||||
|
@cypher(
|
||||||
|
statement: "MATCH (this)<-[obs:OBSERVES]-(u:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(u) >= 1"
|
||||||
|
)
|
||||||
|
observingUsersCount: Int!
|
||||||
|
@cypher(statement: "MATCH (this)<-[obs:OBSERVES]-(u:User) WHERE obs.active = true RETURN COUNT(DISTINCT u)")
|
||||||
}
|
}
|
||||||
|
|
||||||
input _PostInput {
|
input _PostInput {
|
||||||
@ -239,6 +246,8 @@ type Mutation {
|
|||||||
shout(id: ID!, type: ShoutTypeEnum): Boolean!
|
shout(id: ID!, type: ShoutTypeEnum): Boolean!
|
||||||
# Unshout the given Type and ID
|
# Unshout the given Type and ID
|
||||||
unshout(id: ID!, type: ShoutTypeEnum): Boolean!
|
unshout(id: ID!, type: ShoutTypeEnum): Boolean!
|
||||||
|
|
||||||
|
toggleObservePost(id: ID!, value: Boolean!): Post!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
|||||||
@ -67,6 +67,8 @@ export const postFragment = gql`
|
|||||||
}
|
}
|
||||||
pinnedAt
|
pinnedAt
|
||||||
pinned
|
pinned
|
||||||
|
isObservedByMe
|
||||||
|
observingUsersCount
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -175,5 +175,13 @@ export default () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
toggleObservePost: gql`
|
||||||
|
mutation ($id: ID!, $value: Boolean!) {
|
||||||
|
toggleObservePost(id: $id, value: $value) {
|
||||||
|
isObservedByMe
|
||||||
|
observingUsersCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user