Merge pull request #5199 from Ocelot-Social-Community/5059-groups/5188-query-members-of-group

feat: 🍰 Implement `JoinGroup`, `GroupMember`, `SwitchGroupMemberRole` Resolvers
This commit is contained in:
Wolfgang Huß 2022-08-30 10:28:26 +02:00 committed by GitHub
commit 244cb590e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 2407 additions and 241 deletions

View File

@ -39,6 +39,28 @@ export const createGroupMutation = gql`
}
`
export const joinGroupMutation = gql`
mutation ($groupId: ID!, $userId: ID!) {
JoinGroup(groupId: $groupId, userId: $userId) {
id
name
slug
myRoleInGroup
}
}
`
export const changeGroupMemberRoleMutation = gql`
mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) {
ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) {
id
name
slug
myRoleInGroup
}
}
`
// ------ queries
export const groupQuery = gql`
@ -93,3 +115,14 @@ export const groupQuery = gql`
}
}
`
export const groupMembersQuery = gql`
query ($id: ID!, $first: Int, $offset: Int, $orderBy: [_UserOrdering], $filter: _UserFilter) {
GroupMembers(id: $id, first: $first, offset: $offset, orderBy: $orderBy, filter: $filter) {
id
name
slug
myRoleInGroup
}
}
`

View File

@ -5,7 +5,11 @@ import createServer from '../server'
import faker from '@faker-js/faker'
import Factory from '../db/factories'
import { getNeode, getDriver } from '../db/neo4j'
import { createGroupMutation } from './graphql/groups'
import {
createGroupMutation,
joinGroupMutation,
changeGroupMemberRoleMutation,
} from './graphql/groups'
import { createPostMutation } from './graphql/posts'
import { createCommentMutation } from './graphql/comments'
@ -400,6 +404,62 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
])
await Promise.all([
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g0',
userId: 'u2',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g0',
userId: 'u3',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g0',
userId: 'u4',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g0',
userId: 'u6',
},
}),
])
await Promise.all([
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u2',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u4',
roleInGroup: 'admin',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u3',
roleInGroup: 'owner',
},
}),
])
authenticatedUser = await jennyRostock.toJson()
await Promise.all([
@ -416,6 +476,77 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
])
await Promise.all([
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g1',
userId: 'u1',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g1',
userId: 'u2',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g1',
userId: 'u5',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g1',
userId: 'u6',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g1',
userId: 'u7',
},
}),
])
await Promise.all([
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u1',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u2',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u5',
roleInGroup: 'admin',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u6',
roleInGroup: 'owner',
},
}),
])
authenticatedUser = await bobDerBaumeister.toJson()
await Promise.all([
@ -432,6 +563,62 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
])
await Promise.all([
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g2',
userId: 'u4',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g2',
userId: 'u5',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g2',
userId: 'u6',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g2',
userId: 'u7',
},
}),
])
await Promise.all([
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u4',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u5',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u6',
roleInGroup: 'usual',
},
}),
])
// Create Posts

View File

@ -7,3 +7,12 @@
export function gql(strings) {
return strings.join('')
}
// sometime we have to wait to check a db state by having a look into the db in a certain moment
// or we wait a bit to check if we missed to set an await somewhere
// see: https://www.sitepoint.com/delay-sleep-pause-wait/
export function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// usage 4 seconds for example
// await sleep(4 * 1000)

View File

@ -52,6 +52,115 @@ const isMySocialMedia = rule({
return socialMedia.ownedBy.node.id === user.id
})
const isAllowedSeeingMembersOfGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { id: groupId } = args
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (group:Group {id: $groupId})
OPTIONAL MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group)
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
`,
{ groupId, userId: user.id },
)
return {
member: transactionResponse.records.map((record) => record.get('member'))[0],
group: transactionResponse.records.map((record) => record.get('group'))[0],
}
})
try {
const { member, group } = await readTxPromise
return (
!!group &&
(group.groupType === 'public' ||
(['closed', 'hidden'].includes(group.groupType) &&
!!member &&
['usual', 'admin', 'owner'].includes(member.myRoleInGroup)))
)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAllowedToChangeGroupMemberRole = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const adminId = user.id
const { groupId, userId, roleInGroup } = args
if (adminId === userId) return false
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (admin:User {id: $adminId})-[adminMembership:MEMBER_OF]->(group:Group {id: $groupId})
OPTIONAL MATCH (group)<-[userMembership:MEMBER_OF]-(member:User {id: $userId})
RETURN group {.*}, admin {.*, myRoleInGroup: adminMembership.role}, member {.*, myRoleInGroup: userMembership.role}
`,
{ groupId, adminId, userId },
)
return {
admin: transactionResponse.records.map((record) => record.get('admin'))[0],
group: transactionResponse.records.map((record) => record.get('group'))[0],
member: transactionResponse.records.map((record) => record.get('member'))[0],
}
})
try {
const { admin, group, member } = await readTxPromise
return (
!!group &&
!!admin &&
(!member ||
(!!member &&
(member.myRoleInGroup === roleInGroup || !['owner'].includes(member.myRoleInGroup)))) &&
((['admin'].includes(admin.myRoleInGroup) &&
['pending', 'usual', 'admin'].includes(roleInGroup)) ||
(['owner'].includes(admin.myRoleInGroup) &&
['pending', 'usual', 'admin', 'owner'].includes(roleInGroup)))
)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAllowedToJoinGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { groupId, userId } = args
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (group:Group {id: $groupId})
OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(member:User {id: $userId})
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
`,
{ groupId, userId },
)
return {
group: transactionResponse.records.map((record) => record.get('group'))[0],
member: transactionResponse.records.map((record) => record.get('member'))[0],
}
})
try {
const { group, member } = await readTxPromise
return !!group && (group.groupType !== 'hidden' || (!!member && !!member.myRoleInGroup))
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAuthor = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
@ -78,7 +187,7 @@ const isAuthor = rule({
const isDeletingOwnAccount = rule({
cache: 'no_cache',
})(async (parent, args, context, info) => {
})(async (parent, args, context, _info) => {
return context.user.id === args.id
})
@ -115,6 +224,7 @@ export default shield(
statistics: allow,
currentUser: allow,
Group: isAuthenticated,
GroupMembers: isAllowedSeeingMembersOfGroup,
Post: allow,
profilePagePosts: allow,
Comment: allow,
@ -142,6 +252,8 @@ export default shield(
SignupVerification: allow,
UpdateUser: onlyYourself,
CreateGroup: isAuthenticated,
JoinGroup: isAllowedToJoinGroup,
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
CreatePost: isAuthenticated,
UpdatePost: isAuthor,
DeletePost: isAuthor,

View File

@ -46,6 +46,27 @@ export default {
session.close()
}
},
GroupMembers: async (_object, params, context, _resolveInfo) => {
const { id: groupId } = params
const session = context.driver.session()
const readTxResultPromise = session.readTransaction(async (txc) => {
const groupMemberCypher = `
MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId})
RETURN user {.*, myRoleInGroup: membership.role}
`
const result = await txc.run(groupMemberCypher, {
groupId,
})
return result.records.map((record) => record.get('user'))
})
try {
return await readTxResultPromise
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
},
},
Mutation: {
CreateGroup: async (_parent, params, context, _resolveInfo) => {
@ -86,9 +107,10 @@ export default {
MATCH (owner:User {id: $userId})
MERGE (owner)-[:CREATED]->(group)
MERGE (owner)-[membership:MEMBER_OF]->(group)
SET membership.createdAt = toString(datetime())
SET membership.updatedAt = toString(datetime())
SET membership.role = 'owner'
SET
membership.createdAt = toString(datetime()),
membership.updatedAt = null,
membership.role = 'owner'
${categoriesCypher}
RETURN group {.*, myRole: membership.role}
`,
@ -100,8 +122,7 @@ export default {
return group
})
try {
const group = await writeTxResultPromise
return group
return await writeTxResultPromise
} catch (error) {
if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
throw new UserInputError('Group with this slug already exists!')
@ -110,6 +131,63 @@ export default {
session.close()
}
},
JoinGroup: async (_parent, params, context, _resolveInfo) => {
const { groupId, userId } = params
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const joinGroupCypher = `
MATCH (member:User {id: $userId}), (group:Group {id: $groupId})
MERGE (member)-[membership:MEMBER_OF]->(group)
ON CREATE SET
membership.createdAt = toString(datetime()),
membership.updatedAt = null,
membership.role =
CASE WHEN group.groupType = 'public'
THEN 'usual'
ELSE 'pending'
END
RETURN member {.*, myRoleInGroup: membership.role}
`
const result = await transaction.run(joinGroupCypher, { groupId, userId })
const [member] = await result.records.map((record) => record.get('member'))
return member
})
try {
return await writeTxResultPromise
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
},
ChangeGroupMemberRole: async (_parent, params, context, _resolveInfo) => {
const { groupId, userId, roleInGroup } = params
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const joinGroupCypher = `
MATCH (member:User {id: $userId}), (group:Group {id: $groupId})
MERGE (member)-[membership:MEMBER_OF]->(group)
ON CREATE SET
membership.createdAt = toString(datetime()),
membership.updatedAt = null,
membership.role = $roleInGroup
ON MATCH SET
membership.updatedAt = toString(datetime()),
membership.role = $roleInGroup
RETURN member {.*, myRoleInGroup: membership.role}
`
const result = await transaction.run(joinGroupCypher, { groupId, userId, roleInGroup })
const [member] = await result.records.map((record) => record.get('member'))
return member
})
try {
return await writeTxResultPromise
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
},
},
Group: {
...Resolver('Group', {

File diff suppressed because it is too large Load Diff

View File

@ -59,7 +59,7 @@ input _GroupFilter {
type Query {
Group(
isMember: Boolean # if 'undefined' or 'null' then all groups
isMember: Boolean # if 'undefined' or 'null' then get all groups
id: ID
name: String
slug: String
@ -71,14 +71,21 @@ type Query {
first: Int
offset: Int
orderBy: [_GroupOrdering]
filter: _GroupFilter
): [Group]
AvailableGroupTypes: [GroupType]!
GroupMembers(
id: ID!
first: Int
offset: Int
orderBy: [_UserOrdering]
filter: _UserFilter
): [User]
AvailableGroupActionRadii: [GroupActionRadius]!
# AvailableGroupTypes: [GroupType]!
AvailableGroupMemberRoles: [GroupMemberRole]!
# AvailableGroupActionRadii: [GroupActionRadius]!
# AvailableGroupMemberRoles: [GroupMemberRole]!
}
type Mutation {
@ -95,15 +102,26 @@ type Mutation {
locationName: String
): Group
UpdateGroup(
id: ID!
name: String
slug: String
avatar: ImageInput
locationName: String
about: String
description: String
): Group
# UpdateGroup(
# id: ID!
# name: String
# slug: String
# avatar: ImageInput
# locationName: String
# about: String
# description: String
# ): Group
DeleteGroup(id: ID!): Group
# DeleteGroup(id: ID!): Group
JoinGroup(
groupId: ID!
userId: ID!
): User
ChangeGroupMemberRole(
groupId: ID!
userId: ID!
roleInGroup: GroupMemberRole!
): User
}

View File

@ -114,6 +114,8 @@ type User {
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
emotions: [EMOTED]
myRoleInGroup: GroupMemberRole
}