diff --git a/backend/src/db/graphql/groups.js b/backend/src/db/graphql/groups.js index ff63f1a25..c5c6756ed 100644 --- a/backend/src/db/graphql/groups.js +++ b/backend/src/db/graphql/groups.js @@ -120,36 +120,8 @@ export const changeGroupMemberRoleMutation = gql` // ------ queries export const groupQuery = gql` - query ( - $isMember: Boolean - $id: ID - $name: String - $slug: String - $createdAt: String - $updatedAt: String - $about: String - $description: String - $locationName: String - $first: Int - $offset: Int - $orderBy: [_GroupOrdering] - $filter: _GroupFilter - ) { - Group( - isMember: $isMember - id: $id - name: $name - slug: $slug - createdAt: $createdAt - updatedAt: $updatedAt - about: $about - description: $description - locationName: $locationName - first: $first - offset: $offset - orderBy: $orderBy - filter: $filter - ) { + query ($isMember: Boolean, $id: ID, $slug: String) { + Group(isMember: $isMember, id: $id, slug: $slug) { id name slug diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 239a299dd..6a5bd7966 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -4,20 +4,25 @@ import CONFIG from '../../config' import { CATEGORIES_MIN, CATEGORIES_MAX } from '../../constants/categories' import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' import { removeHtmlTags } from '../../middleware/helpers/cleanHtml.js' -import Resolver from './helpers/Resolver' +import Resolver, { + removeUndefinedNullValuesFromObject, + convertObjectToCypherMapLiteral, +} from './helpers/Resolver' import { mergeImage } from './images/images' export default { Query: { Group: async (_object, params, context, _resolveInfo) => { - const { id: groupId, isMember } = params + const { isMember, id, slug } = params + const matchParams = { id, slug } + removeUndefinedNullValuesFromObject(matchParams) const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { - const groupIdCypher = groupId ? ` {id: "${groupId}"}` : '' + const groupMatchParamsCypher = convertObjectToCypherMapLiteral(matchParams, true) let groupCypher if (isMember === true) { groupCypher = ` - MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group${groupIdCypher}) + MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group${groupMatchParamsCypher}) WITH group, membership WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner']) RETURN group {.*, myRole: membership.role} @@ -25,7 +30,7 @@ export default { } else { if (isMember === false) { groupCypher = ` - MATCH (group:Group${groupIdCypher}) + MATCH (group:Group${groupMatchParamsCypher}) WHERE (NOT (:User {id: $userId})-[:MEMBER_OF]->(group)) WITH group WHERE group.groupType IN ['public', 'closed'] @@ -33,7 +38,7 @@ export default { ` } else { groupCypher = ` - MATCH (group:Group${groupIdCypher}) + MATCH (group:Group${groupMatchParamsCypher}) OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) WITH group, membership WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner']) diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index e9b38cc2b..03b49e64a 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -503,6 +503,72 @@ describe('in mode', () => { }) }) + describe('with given slug', () => { + describe("slug = 'the-best-group'", () => { + it('finds only the listed group with this slug', async () => { + const result = await query({ + query: groupQuery, + variables: { slug: 'the-best-group' }, + }) + expect(result).toMatchObject({ + data: { + Group: [ + expect.objectContaining({ + id: 'my-group', + slug: 'the-best-group', + myRole: 'owner', + }), + ], + }, + errors: undefined, + }) + expect(result.data.Group.length).toBe(1) + }) + }) + + describe("slug = 'third-investigative-journalism-group'", () => { + it("finds only the hidden group where I'm 'usual' member", async () => { + const result = await query({ + query: groupQuery, + variables: { slug: 'third-investigative-journalism-group' }, + }) + expect(result).toMatchObject({ + data: { + Group: expect.arrayContaining([ + expect.objectContaining({ + id: 'third-hidden-group', + slug: 'third-investigative-journalism-group', + myRole: 'usual', + }), + ]), + }, + errors: undefined, + }) + expect(result.data.Group.length).toBe(1) + }) + }) + + describe("slug = 'second-investigative-journalism-group'", () => { + it("finds no hidden group where I'm 'pending' member", async () => { + const result = await query({ + query: groupQuery, + variables: { slug: 'second-investigative-journalism-group' }, + }) + expect(result.data.Group.length).toBe(0) + }) + }) + + describe("slug = 'investigative-journalism-group'", () => { + it("finds no hidden group where I'm not(!) a member at all", async () => { + const result = await query({ + query: groupQuery, + variables: { slug: 'investigative-journalism-group' }, + }) + expect(result.data.Group.length).toBe(0) + }) + }) + }) + describe('isMember = true', () => { it('finds only listed groups where user is member', async () => { const result = await query({ query: groupQuery, variables: { isMember: true } }) diff --git a/backend/src/schema/resolvers/helpers/Resolver.js b/backend/src/schema/resolvers/helpers/Resolver.js index f2861e7a0..6e8211521 100644 --- a/backend/src/schema/resolvers/helpers/Resolver.js +++ b/backend/src/schema/resolvers/helpers/Resolver.js @@ -121,3 +121,25 @@ export default function Resolver(type, options = {}) { } return result } + +export const removeUndefinedNullValuesFromObject = (obj) => { + Object.keys(obj).forEach((key) => { + if ([undefined, null].includes(obj[key])) { + delete obj[key] + } + }) +} + +export const convertObjectToCypherMapLiteral = (params, addSpaceInfrontIfMapIsNotEmpty = false) => { + // I have found no other way yet. maybe "apoc.convert.fromJsonMap(key)" can help, but couldn't get it how, see: https://stackoverflow.com/questions/43217823/neo4j-cypher-inline-conversion-of-string-to-a-map + // result looks like: '{id: "g0", slug: "yoga"}' + const paramsEntries = Object.entries(params) + let mapLiteral = '' + paramsEntries.forEach((ele, index) => { + mapLiteral += index === 0 ? '{' : '' + mapLiteral += `${ele[0]}: "${ele[1]}"` + mapLiteral += index < paramsEntries.length - 1 ? ', ' : '}' + }) + mapLiteral = (addSpaceInfrontIfMapIsNotEmpty && mapLiteral.length > 0 ? ' ' : '') + mapLiteral + return mapLiteral +} diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index c1b097857..8c881ef1d 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -61,27 +61,19 @@ type Query { Group( isMember: Boolean # if 'undefined' or 'null' then get all groups id: ID - name: String slug: String - createdAt: String - updatedAt: String - about: String - description: String - # groupType: GroupType # test this - # actionRadius: GroupActionRadius # test this - # avatar: ImageInput # test this - locationName: String - first: Int - offset: Int - orderBy: [_GroupOrdering] + # first: Int # not implemented yet + # offset: Int # not implemented yet + # orderBy: [_GroupOrdering] # not implemented yet + # filter: _GroupFilter # not implemented yet ): [Group] GroupMembers( id: ID! - first: Int - offset: Int - orderBy: [_UserOrdering] - filter: _UserFilter + # first: Int # not implemented yet + # offset: Int # not implemented yet + # orderBy: [_UserOrdering] # not implemented yet + # filter: _UserFilter # not implemented yet ): [User] # AvailableGroupTypes: [GroupType]! diff --git a/webapp/graphql/groups.js b/webapp/graphql/groups.js index ff63f1a25..c5c6756ed 100644 --- a/webapp/graphql/groups.js +++ b/webapp/graphql/groups.js @@ -120,36 +120,8 @@ export const changeGroupMemberRoleMutation = gql` // ------ queries export const groupQuery = gql` - query ( - $isMember: Boolean - $id: ID - $name: String - $slug: String - $createdAt: String - $updatedAt: String - $about: String - $description: String - $locationName: String - $first: Int - $offset: Int - $orderBy: [_GroupOrdering] - $filter: _GroupFilter - ) { - Group( - isMember: $isMember - id: $id - name: $name - slug: $slug - createdAt: $createdAt - updatedAt: $updatedAt - about: $about - description: $description - locationName: $locationName - first: $first - offset: $offset - orderBy: $orderBy - filter: $filter - ) { + query ($isMember: Boolean, $id: ID, $slug: String) { + Group(isMember: $isMember, id: $id, slug: $slug) { id name slug diff --git a/webapp/mixins/persistentLinks.js b/webapp/mixins/persistentLinks.js index 0abe37c03..164ccea41 100644 --- a/webapp/mixins/persistentLinks.js +++ b/webapp/mixins/persistentLinks.js @@ -10,22 +10,24 @@ export default function (options = {}) { } = context const idOrSlug = id || slug - const variables = { idOrSlug } - const client = apolloProvider.defaultClient + if (idOrSlug) { + const variables = { idOrSlug } + const client = apolloProvider.defaultClient - let response - let resource - response = await client.query({ query: queryId, variables }) - resource = response.data[Object.keys(response.data)[0]][0] - if (resource && resource.slug === slug) return // all good - if (resource && resource.slug !== slug) { - return redirect(`/${path}/${resource.id}/${resource.slug}`) + let response + let resource + response = await client.query({ query: queryId, variables }) + resource = response.data[Object.keys(response.data)[0]][0] + if (resource && resource.slug === slug) return // all good + if (resource && resource.slug !== slug) { + return redirect(`/${path}/${resource.id}/${resource.slug}`) + } + + response = await client.query({ query: querySlug, variables }) + resource = response.data[Object.keys(response.data)[0]][0] + if (resource) return redirect(`/${path}/${resource.id}/${resource.slug}`) } - response = await client.query({ query: querySlug, variables }) - resource = response.data[Object.keys(response.data)[0]][0] - if (resource) return redirect(`/${path}/${resource.id}/${resource.slug}`) - return error({ statusCode: 404, key: message }) }, }