Skip to content

feat: 🍰 Post In Groups #5380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
90e1696
add group to post in schema
Mogge Sep 20, 2022
f331813
create post in group
Mogge Sep 20, 2022
0c32fd8
add post access filter for all post queries
Mogge Sep 20, 2022
5e30bfe
add direction of Post in Group
Mogge Sep 20, 2022
4d480d3
add post query
Mogge Sep 22, 2022
e748fcc
add is member of group check to permission of create post
Mogge Sep 22, 2022
a9cd661
add tets for posts in groups
Mogge Sep 22, 2022
b259295
Merge branch '5059-epic-groups' into post-in-group
Mogge Oct 3, 2022
1870efc
add CANNOT_SEE relation to posts in groups
Mogge Oct 3, 2022
5f4d7c2
add notVisibleFor statement
Mogge Oct 3, 2022
abcfe25
query and mutations are functions
Mogge Oct 3, 2022
4a63e14
calls to query and mutate are functions
Mogge Oct 3, 2022
4a12cdf
remove unused memberIds statement
Mogge Oct 3, 2022
069eefd
add helper function to filter invisible posts
Mogge Oct 3, 2022
833bd5f
use invisible post filter
Mogge Oct 4, 2022
16a24bf
remove not working filters from schema
Mogge Oct 4, 2022
32d3c5e
insure that user context is present as posts can be queried without a…
Mogge Oct 4, 2022
4331c73
test post invisibility for unauthenticated users
Mogge Oct 4, 2022
77125e4
createPostMutation as function
Mogge Oct 4, 2022
068d622
createPostMutation as function
Mogge Oct 4, 2022
275d196
further testting for post visibility
Mogge Oct 4, 2022
0343407
fix cypher to have signup verification to block posts of groups for n…
Mogge Oct 4, 2022
67cba10
remove findPosts and findUsers as they are never used.
Mogge Oct 5, 2022
570d6c0
test filter posts
Mogge Oct 5, 2022
ab5d308
test profile page posts
Mogge Oct 5, 2022
aa870b5
searches need authorization as they are not working without user id i…
Mogge Oct 5, 2022
76bfe48
implement and test post visibilty when leaving or changing the role i…
Mogge Oct 5, 2022
a924357
fix cypher when posts do not exist
Mogge Oct 5, 2022
74505a1
filter posts for group visibility on search posts
Mogge Oct 5, 2022
a4cd7a8
test search posts with groups
Mogge Oct 5, 2022
4fdaa0d
Update backend/src/schema/resolvers/helpers/filterInvisiblePosts.js
Mogge Oct 6, 2022
4556276
fix: leaving public group keeps the posts in public group visible. Te…
Mogge Oct 10, 2022
631f34a
improved code and tests as suggested by @tirokk, thanks for the great…
Mogge Oct 10, 2022
7cb1663
change from role eq pending to usual, admin, owner includes role
Mogge Oct 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/src/db/graphql/authentications.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const signupVerificationMutation = gql`
nonce: $nonce
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
) {
id
slug
}
}
Expand Down
88 changes: 80 additions & 8 deletions backend/src/db/graphql/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,87 @@ import gql from 'graphql-tag'

// ------ mutations

export const createPostMutation = gql`
mutation ($id: ID, $title: String!, $slug: String, $content: String!, $categoryIds: [ID]!) {
CreatePost(id: $id, title: $title, slug: $slug, content: $content, categoryIds: $categoryIds) {
id
slug
export const createPostMutation = () => {
return gql`
mutation (
$id: ID
$title: String!
$slug: String
$content: String!
$categoryIds: [ID]
$groupId: ID
) {
CreatePost(
id: $id
title: $title
slug: $slug
content: $content
categoryIds: $categoryIds
groupId: $groupId
) {
id
slug
title
content
}
}
}
`
`
}

// ------ queries

// fill queries in here
export const postQuery = () => {
return gql`
query Post($id: ID!) {
Post(id: $id) {
id
title
content
}
}
`
}

export const filterPosts = () => {
return gql`
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
id
title
content
}
}
`
}

export const profilePagePosts = () => {
return gql`
query profilePagePosts(
$filter: _PostFilter
$first: Int
$offset: Int
$orderBy: [_PostOrdering]
) {
profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
id
title
content
}
}
`
}

export const searchPosts = () => {
return gql`
query ($query: String!, $firstPosts: Int, $postsOffset: Int) {
searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
postCount
posts {
id
title
content
}
}
}
`
}
8 changes: 4 additions & 4 deletions backend/src/db/seed.js
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']

await Promise.all([
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables: {
id: 'p2',
title: `Nature Philosophy Yoga`,
Expand All @@ -718,7 +718,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables: {
id: 'p7',
title: 'This is post #7',
Expand All @@ -727,7 +727,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables: {
id: 'p8',
image: faker.image.unsplash.nature(),
Expand All @@ -737,7 +737,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables: {
id: 'p12',
title: 'This is post #12',
Expand Down
34 changes: 30 additions & 4 deletions backend/src/middleware/permissionsMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { rule, shield, deny, allow, or } from 'graphql-shield'
import { rule, shield, deny, allow, or, and } from 'graphql-shield'
import { getNeode } from '../db/neo4j'
import CONFIG from '../config'
import { validateInviteCode } from '../schema/resolvers/transactions/inviteCodes'
Expand Down Expand Up @@ -221,6 +221,34 @@ const isAllowedToLeaveGroup = rule({
}
})

const isMemberOfGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { groupId } = args
if (!groupId) return true
const userId = user.id
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (User {id: $userId})-[membership:MEMBER_OF]->(Group {id: $groupId})
RETURN membership.role AS role
`,
{ groupId, userId },
)
return transactionResponse.records.map((record) => record.get('role'))[0]
})
try {
const role = await readTxPromise
return ['usual', 'admin', 'owner'].includes(role)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})

const isAuthor = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
Expand Down Expand Up @@ -271,8 +299,6 @@ export default shield(
{
Query: {
'*': deny,
findPosts: allow,
findUsers: allow,
searchResults: allow,
searchPosts: allow,
searchUsers: allow,
Expand Down Expand Up @@ -316,7 +342,7 @@ export default shield(
JoinGroup: isAllowedToJoinGroup,
LeaveGroup: isAllowedToLeaveGroup,
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
CreatePost: isAuthenticated,
CreatePost: and(isAuthenticated, isMemberOfGroup),
UpdatePost: isAuthor,
DeletePost: isAuthor,
fileReport: isAuthenticated,
Expand Down
8 changes: 4 additions & 4 deletions backend/src/middleware/slugifyMiddleware.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ describe('slugifyMiddleware', () => {
it('generates a slug based on title', async () => {
await expect(
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables,
}),
).resolves.toMatchObject({
Expand All @@ -382,7 +382,7 @@ describe('slugifyMiddleware', () => {
it('generates a slug based on given slug', async () => {
await expect(
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables: {
...variables,
slug: 'the-post',
Expand Down Expand Up @@ -417,7 +417,7 @@ describe('slugifyMiddleware', () => {
it('chooses another slug', async () => {
await expect(
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables: {
...variables,
title: 'Pre-existing post',
Expand All @@ -440,7 +440,7 @@ describe('slugifyMiddleware', () => {
try {
await expect(
mutate({
mutation: createPostMutation,
mutation: createPostMutation(),
variables: {
...variables,
title: 'Pre-existing post',
Expand Down
2 changes: 1 addition & 1 deletion backend/src/middleware/userInteractions.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const setPostCounter = async (postId, relation, context) => {
}

const userClickedPost = async (resolve, root, args, context, info) => {
if (args.id) {
if (args.id && context.user) {
await setPostCounter(args.id, 'CLICKED', context)
}
return resolve(root, args, context, info)
Expand Down
26 changes: 25 additions & 1 deletion backend/src/schema/resolvers/groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,15 @@ export default {
const leaveGroupCypher = `
MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
DELETE membership
WITH member, group
OPTIONAL MATCH (p:Post)-[:IN]->(group)
WHERE NOT group.groupType = 'public'
WITH member, group, collect(p) AS posts
FOREACH (post IN posts |
MERGE (member)-[:CANNOT_SEE]->(post))
RETURN member {.*, myRoleInGroup: NULL}
`

const transactionResponse = await transaction.run(leaveGroupCypher, { groupId, userId })
const [member] = await transactionResponse.records.map((record) => record.get('member'))
return member
Expand All @@ -279,8 +286,22 @@ export default {
const { groupId, userId, roleInGroup } = params
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
let postRestrictionCypher = ''
if (['usual', 'admin', 'owner'].includes(roleInGroup)) {
postRestrictionCypher = `
WITH group, member, membership
FOREACH (restriction IN [(member)-[r:CANNOT_SEE]->(:Post)-[:IN]->(group) | r] |
DELETE restriction)`
} else {
postRestrictionCypher = `
WITH group, member, membership
FOREACH (post IN [(p:Post)-[:IN]->(group) | p] |
MERGE (member)-[:CANNOT_SEE]->(post))`
}

const joinGroupCypher = `
MATCH (member:User {id: $userId}), (group:Group {id: $groupId})
MATCH (member:User {id: $userId})
MATCH (group:Group {id: $groupId})
MERGE (member)-[membership:MEMBER_OF]->(group)
ON CREATE SET
membership.createdAt = toString(datetime()),
Expand All @@ -289,8 +310,10 @@ export default {
ON MATCH SET
membership.updatedAt = toString(datetime()),
membership.role = $roleInGroup
${postRestrictionCypher}
RETURN member {.*, myRoleInGroup: membership.role}
`

const transactionResponse = await transaction.run(joinGroupCypher, {
groupId,
userId,
Expand All @@ -313,6 +336,7 @@ export default {
undefinedToNull: ['deleted', 'disabled', 'locationName', 'about'],
hasMany: {
categories: '-[:CATEGORIZED]->(related:Category)',
posts: '<-[:IN]-(related:Post)',
},
hasOne: {
avatar: '-[:AVATAR_IMAGE]->(related:Image)',
Expand Down
47 changes: 47 additions & 0 deletions backend/src/schema/resolvers/helpers/filterInvisiblePosts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { mergeWith, isArray } from 'lodash'

const getInvisiblePosts = async (context) => {
const session = context.driver.session()
const readTxResultPromise = await session.readTransaction(async (transaction) => {
let cypher = ''
const { user } = context
if (user && user.id) {
cypher = `
MATCH (post:Post)<-[:CANNOT_SEE]-(user:User { id: $userId })
RETURN collect(post.id) AS invisiblePostIds`
} else {
cypher = `
MATCH (post:Post)-[:IN]->(group:Group)
WHERE NOT group.groupType = 'public'
RETURN collect(post.id) AS invisiblePostIds`
}
const invisiblePostIdsResponse = await transaction.run(cypher, {
userId: user ? user.id : null,
})
return invisiblePostIdsResponse.records.map((record) => record.get('invisiblePostIds'))
})
try {
const [invisiblePostIds] = readTxResultPromise
return invisiblePostIds
} finally {
session.close()
}
}

export const filterInvisiblePosts = async (params, context) => {
const invisiblePostIds = await getInvisiblePosts(context)
if (!invisiblePostIds.length) return params

params.filter = mergeWith(
params.filter,
{
id_not_in: invisiblePostIds,
},
(objValue, srcValue) => {
if (isArray(objValue)) {
return objValue.concat(srcValue)
}
},
)
return params
}
Loading