Skip to content

Commit 51ce290

Browse files
authored
Merge pull request #5380 from Ocelot-Social-Community/post-in-group
feat: 🍰 Post In Groups
2 parents 356627a + 7cb1663 commit 51ce290

15 files changed

+1769
-67
lines changed

backend/src/db/graphql/authentications.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const signupVerificationMutation = gql`
1919
nonce: $nonce
2020
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
2121
) {
22+
id
2223
slug
2324
}
2425
}

backend/src/db/graphql/posts.js

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,87 @@ import gql from 'graphql-tag'
22

33
// ------ mutations
44

5-
export const createPostMutation = gql`
6-
mutation ($id: ID, $title: String!, $slug: String, $content: String!, $categoryIds: [ID]!) {
7-
CreatePost(id: $id, title: $title, slug: $slug, content: $content, categoryIds: $categoryIds) {
8-
id
9-
slug
5+
export const createPostMutation = () => {
6+
return gql`
7+
mutation (
8+
$id: ID
9+
$title: String!
10+
$slug: String
11+
$content: String!
12+
$categoryIds: [ID]
13+
$groupId: ID
14+
) {
15+
CreatePost(
16+
id: $id
17+
title: $title
18+
slug: $slug
19+
content: $content
20+
categoryIds: $categoryIds
21+
groupId: $groupId
22+
) {
23+
id
24+
slug
25+
title
26+
content
27+
}
1028
}
11-
}
12-
`
29+
`
30+
}
1331

1432
// ------ queries
1533

16-
// fill queries in here
34+
export const postQuery = () => {
35+
return gql`
36+
query Post($id: ID!) {
37+
Post(id: $id) {
38+
id
39+
title
40+
content
41+
}
42+
}
43+
`
44+
}
45+
46+
export const filterPosts = () => {
47+
return gql`
48+
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
49+
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
50+
id
51+
title
52+
content
53+
}
54+
}
55+
`
56+
}
57+
58+
export const profilePagePosts = () => {
59+
return gql`
60+
query profilePagePosts(
61+
$filter: _PostFilter
62+
$first: Int
63+
$offset: Int
64+
$orderBy: [_PostOrdering]
65+
) {
66+
profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
67+
id
68+
title
69+
content
70+
}
71+
}
72+
`
73+
}
74+
75+
export const searchPosts = () => {
76+
return gql`
77+
query ($query: String!, $firstPosts: Int, $postsOffset: Int) {
78+
searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
79+
postCount
80+
posts {
81+
id
82+
title
83+
content
84+
}
85+
}
86+
}
87+
`
88+
}

backend/src/db/seed.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
709709

710710
await Promise.all([
711711
mutate({
712-
mutation: createPostMutation,
712+
mutation: createPostMutation(),
713713
variables: {
714714
id: 'p2',
715715
title: `Nature Philosophy Yoga`,
@@ -718,7 +718,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
718718
},
719719
}),
720720
mutate({
721-
mutation: createPostMutation,
721+
mutation: createPostMutation(),
722722
variables: {
723723
id: 'p7',
724724
title: 'This is post #7',
@@ -727,7 +727,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
727727
},
728728
}),
729729
mutate({
730-
mutation: createPostMutation,
730+
mutation: createPostMutation(),
731731
variables: {
732732
id: 'p8',
733733
image: faker.image.unsplash.nature(),
@@ -737,7 +737,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
737737
},
738738
}),
739739
mutate({
740-
mutation: createPostMutation,
740+
mutation: createPostMutation(),
741741
variables: {
742742
id: 'p12',
743743
title: 'This is post #12',

backend/src/middleware/permissionsMiddleware.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { rule, shield, deny, allow, or } from 'graphql-shield'
1+
import { rule, shield, deny, allow, or, and } from 'graphql-shield'
22
import { getNeode } from '../db/neo4j'
33
import CONFIG from '../config'
44
import { validateInviteCode } from '../schema/resolvers/transactions/inviteCodes'
@@ -221,6 +221,34 @@ const isAllowedToLeaveGroup = rule({
221221
}
222222
})
223223

224+
const isMemberOfGroup = rule({
225+
cache: 'no_cache',
226+
})(async (_parent, args, { user, driver }) => {
227+
if (!(user && user.id)) return false
228+
const { groupId } = args
229+
if (!groupId) return true
230+
const userId = user.id
231+
const session = driver.session()
232+
const readTxPromise = session.readTransaction(async (transaction) => {
233+
const transactionResponse = await transaction.run(
234+
`
235+
MATCH (User {id: $userId})-[membership:MEMBER_OF]->(Group {id: $groupId})
236+
RETURN membership.role AS role
237+
`,
238+
{ groupId, userId },
239+
)
240+
return transactionResponse.records.map((record) => record.get('role'))[0]
241+
})
242+
try {
243+
const role = await readTxPromise
244+
return ['usual', 'admin', 'owner'].includes(role)
245+
} catch (error) {
246+
throw new Error(error)
247+
} finally {
248+
session.close()
249+
}
250+
})
251+
224252
const isAuthor = rule({
225253
cache: 'no_cache',
226254
})(async (_parent, args, { user, driver }) => {
@@ -271,8 +299,6 @@ export default shield(
271299
{
272300
Query: {
273301
'*': deny,
274-
findPosts: allow,
275-
findUsers: allow,
276302
searchResults: allow,
277303
searchPosts: allow,
278304
searchUsers: allow,
@@ -316,7 +342,7 @@ export default shield(
316342
JoinGroup: isAllowedToJoinGroup,
317343
LeaveGroup: isAllowedToLeaveGroup,
318344
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
319-
CreatePost: isAuthenticated,
345+
CreatePost: and(isAuthenticated, isMemberOfGroup),
320346
UpdatePost: isAuthor,
321347
DeletePost: isAuthor,
322348
fileReport: isAuthenticated,

backend/src/middleware/slugifyMiddleware.spec.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ describe('slugifyMiddleware', () => {
366366
it('generates a slug based on title', async () => {
367367
await expect(
368368
mutate({
369-
mutation: createPostMutation,
369+
mutation: createPostMutation(),
370370
variables,
371371
}),
372372
).resolves.toMatchObject({
@@ -382,7 +382,7 @@ describe('slugifyMiddleware', () => {
382382
it('generates a slug based on given slug', async () => {
383383
await expect(
384384
mutate({
385-
mutation: createPostMutation,
385+
mutation: createPostMutation(),
386386
variables: {
387387
...variables,
388388
slug: 'the-post',
@@ -417,7 +417,7 @@ describe('slugifyMiddleware', () => {
417417
it('chooses another slug', async () => {
418418
await expect(
419419
mutate({
420-
mutation: createPostMutation,
420+
mutation: createPostMutation(),
421421
variables: {
422422
...variables,
423423
title: 'Pre-existing post',
@@ -440,7 +440,7 @@ describe('slugifyMiddleware', () => {
440440
try {
441441
await expect(
442442
mutate({
443-
mutation: createPostMutation,
443+
mutation: createPostMutation(),
444444
variables: {
445445
...variables,
446446
title: 'Pre-existing post',

backend/src/middleware/userInteractions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const setPostCounter = async (postId, relation, context) => {
3131
}
3232

3333
const userClickedPost = async (resolve, root, args, context, info) => {
34-
if (args.id) {
34+
if (args.id && context.user) {
3535
await setPostCounter(args.id, 'CLICKED', context)
3636
}
3737
return resolve(root, args, context, info)

backend/src/schema/resolvers/groups.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,15 @@ export default {
261261
const leaveGroupCypher = `
262262
MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
263263
DELETE membership
264+
WITH member, group
265+
OPTIONAL MATCH (p:Post)-[:IN]->(group)
266+
WHERE NOT group.groupType = 'public'
267+
WITH member, group, collect(p) AS posts
268+
FOREACH (post IN posts |
269+
MERGE (member)-[:CANNOT_SEE]->(post))
264270
RETURN member {.*, myRoleInGroup: NULL}
265271
`
272+
266273
const transactionResponse = await transaction.run(leaveGroupCypher, { groupId, userId })
267274
const [member] = await transactionResponse.records.map((record) => record.get('member'))
268275
return member
@@ -279,8 +286,22 @@ export default {
279286
const { groupId, userId, roleInGroup } = params
280287
const session = context.driver.session()
281288
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
289+
let postRestrictionCypher = ''
290+
if (['usual', 'admin', 'owner'].includes(roleInGroup)) {
291+
postRestrictionCypher = `
292+
WITH group, member, membership
293+
FOREACH (restriction IN [(member)-[r:CANNOT_SEE]->(:Post)-[:IN]->(group) | r] |
294+
DELETE restriction)`
295+
} else {
296+
postRestrictionCypher = `
297+
WITH group, member, membership
298+
FOREACH (post IN [(p:Post)-[:IN]->(group) | p] |
299+
MERGE (member)-[:CANNOT_SEE]->(post))`
300+
}
301+
282302
const joinGroupCypher = `
283-
MATCH (member:User {id: $userId}), (group:Group {id: $groupId})
303+
MATCH (member:User {id: $userId})
304+
MATCH (group:Group {id: $groupId})
284305
MERGE (member)-[membership:MEMBER_OF]->(group)
285306
ON CREATE SET
286307
membership.createdAt = toString(datetime()),
@@ -289,8 +310,10 @@ export default {
289310
ON MATCH SET
290311
membership.updatedAt = toString(datetime()),
291312
membership.role = $roleInGroup
313+
${postRestrictionCypher}
292314
RETURN member {.*, myRoleInGroup: membership.role}
293315
`
316+
294317
const transactionResponse = await transaction.run(joinGroupCypher, {
295318
groupId,
296319
userId,
@@ -313,6 +336,7 @@ export default {
313336
undefinedToNull: ['deleted', 'disabled', 'locationName', 'about'],
314337
hasMany: {
315338
categories: '-[:CATEGORIZED]->(related:Category)',
339+
posts: '<-[:IN]-(related:Post)',
316340
},
317341
hasOne: {
318342
avatar: '-[:AVATAR_IMAGE]->(related:Image)',
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { mergeWith, isArray } from 'lodash'
2+
3+
const getInvisiblePosts = async (context) => {
4+
const session = context.driver.session()
5+
const readTxResultPromise = await session.readTransaction(async (transaction) => {
6+
let cypher = ''
7+
const { user } = context
8+
if (user && user.id) {
9+
cypher = `
10+
MATCH (post:Post)<-[:CANNOT_SEE]-(user:User { id: $userId })
11+
RETURN collect(post.id) AS invisiblePostIds`
12+
} else {
13+
cypher = `
14+
MATCH (post:Post)-[:IN]->(group:Group)
15+
WHERE NOT group.groupType = 'public'
16+
RETURN collect(post.id) AS invisiblePostIds`
17+
}
18+
const invisiblePostIdsResponse = await transaction.run(cypher, {
19+
userId: user ? user.id : null,
20+
})
21+
return invisiblePostIdsResponse.records.map((record) => record.get('invisiblePostIds'))
22+
})
23+
try {
24+
const [invisiblePostIds] = readTxResultPromise
25+
return invisiblePostIds
26+
} finally {
27+
session.close()
28+
}
29+
}
30+
31+
export const filterInvisiblePosts = async (params, context) => {
32+
const invisiblePostIds = await getInvisiblePosts(context)
33+
if (!invisiblePostIds.length) return params
34+
35+
params.filter = mergeWith(
36+
params.filter,
37+
{
38+
id_not_in: invisiblePostIds,
39+
},
40+
(objValue, srcValue) => {
41+
if (isArray(objValue)) {
42+
return objValue.concat(srcValue)
43+
}
44+
},
45+
)
46+
return params
47+
}

0 commit comments

Comments
 (0)