From 36f6fee8be86737266ae0ab795b7907ed90d65d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 9 Sep 2022 13:44:13 +0200 Subject: [PATCH 1/5] Query groups by 'Group' resolver via 'slug' and test it --- backend/src/db/graphql/groups.js | 12 ---- backend/src/schema/resolvers/groups.js | 22 +++++-- backend/src/schema/resolvers/groups.spec.js | 66 +++++++++++++++++++++ backend/src/schema/types/type/Group.gql | 9 --- webapp/graphql/groups.js | 12 ---- 5 files changed, 83 insertions(+), 38 deletions(-) diff --git a/backend/src/db/graphql/groups.js b/backend/src/db/graphql/groups.js index ff63f1a25..3585a7b2d 100644 --- a/backend/src/db/graphql/groups.js +++ b/backend/src/db/graphql/groups.js @@ -123,13 +123,7 @@ 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] @@ -138,13 +132,7 @@ export const groupQuery = gql` 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 diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 239a299dd..73ce56c0c 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -10,14 +10,26 @@ 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 } + Object.keys(matchParams).forEach((key) => { + if ([undefined, null].includes(matchParams[key])) { + delete matchParams[key] + } + }) const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { - const groupIdCypher = groupId ? ` {id: "${groupId}"}` : '' + const matchParamsEntries = Object.entries(matchParams) + let groupMatchParamsCypher = '' + matchParamsEntries.forEach((ele, index) => { + groupMatchParamsCypher += index === 0 ? ' {' : '' + groupMatchParamsCypher += `${ele[0]}: "${ele[1]}"` + groupMatchParamsCypher += index < matchParamsEntries.length - 1 ? ', ' : '}' + }) 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 +37,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 +45,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/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index c1b097857..5cd8821b0 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -61,16 +61,7 @@ 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] diff --git a/webapp/graphql/groups.js b/webapp/graphql/groups.js index ff63f1a25..3585a7b2d 100644 --- a/webapp/graphql/groups.js +++ b/webapp/graphql/groups.js @@ -123,13 +123,7 @@ 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] @@ -138,13 +132,7 @@ export const groupQuery = gql` 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 From af26b8605ca02aae592ab0d7c79cbf4448b744dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 9 Sep 2022 13:49:16 +0200 Subject: [PATCH 2/5] Show page not found error if there is no 'id' and no 'slug' in a persistent link --- webapp/mixins/persistentLinks.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) 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 }) }, } From 5eabdf1495b157ea9de199446e56c840da4423d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Sat, 10 Sep 2022 07:37:00 +0200 Subject: [PATCH 3/5] Comment out unimplemented parameters in 'Group' resolver --- backend/src/db/graphql/groups.js | 20 ++------------------ backend/src/schema/types/type/Group.gql | 15 ++++++++------- webapp/graphql/groups.js | 20 ++------------------ 3 files changed, 12 insertions(+), 43 deletions(-) diff --git a/backend/src/db/graphql/groups.js b/backend/src/db/graphql/groups.js index 3585a7b2d..c5c6756ed 100644 --- a/backend/src/db/graphql/groups.js +++ b/backend/src/db/graphql/groups.js @@ -120,24 +120,8 @@ export const changeGroupMemberRoleMutation = gql` // ------ queries export const groupQuery = gql` - query ( - $isMember: Boolean - $id: ID - $slug: String - $first: Int - $offset: Int - $orderBy: [_GroupOrdering] - $filter: _GroupFilter - ) { - Group( - isMember: $isMember - id: $id - slug: $slug - 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/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index 5cd8821b0..8c881ef1d 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -62,17 +62,18 @@ type Query { isMember: Boolean # if 'undefined' or 'null' then get all groups id: ID slug: 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 3585a7b2d..c5c6756ed 100644 --- a/webapp/graphql/groups.js +++ b/webapp/graphql/groups.js @@ -120,24 +120,8 @@ export const changeGroupMemberRoleMutation = gql` // ------ queries export const groupQuery = gql` - query ( - $isMember: Boolean - $id: ID - $slug: String - $first: Int - $offset: Int - $orderBy: [_GroupOrdering] - $filter: _GroupFilter - ) { - Group( - isMember: $isMember - id: $id - slug: $slug - 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 From 7bb3a05b07fe95ae89d7a626bff265420ea25c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Wed, 14 Sep 2022 16:10:55 +0200 Subject: [PATCH 4/5] Clarify conversion to Cypher literal map for use as 'MATCH' parameter by making a resolver helper function --- backend/src/schema/resolvers/groups.js | 19 ++++++----------- .../src/schema/resolvers/helpers/Resolver.js | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 73ce56c0c..4349ac9bf 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -4,7 +4,10 @@ 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 { @@ -12,20 +15,10 @@ export default { Group: async (_object, params, context, _resolveInfo) => { const { isMember, id, slug } = params const matchParams = { id, slug } - Object.keys(matchParams).forEach((key) => { - if ([undefined, null].includes(matchParams[key])) { - delete matchParams[key] - } - }) + removeUndefinedNullValuesFromObject(matchParams) const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { - const matchParamsEntries = Object.entries(matchParams) - let groupMatchParamsCypher = '' - matchParamsEntries.forEach((ele, index) => { - groupMatchParamsCypher += index === 0 ? ' {' : '' - groupMatchParamsCypher += `${ele[0]}: "${ele[1]}"` - groupMatchParamsCypher += index < matchParamsEntries.length - 1 ? ', ' : '}' - }) + const groupMatchParamsCypher = convertObjectToCypherMapLiteral(matchParams) let groupCypher if (isMember === true) { groupCypher = ` diff --git a/backend/src/schema/resolvers/helpers/Resolver.js b/backend/src/schema/resolvers/helpers/Resolver.js index f2861e7a0..56d382690 100644 --- a/backend/src/schema/resolvers/helpers/Resolver.js +++ b/backend/src/schema/resolvers/helpers/Resolver.js @@ -121,3 +121,24 @@ 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) => { + // 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 ? ', ' : '}' + }) + return mapLiteral +} From 73b6b24b72194367024f480a736e618c0b24c2d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Wed, 14 Sep 2022 16:30:09 +0200 Subject: [PATCH 5/5] Refine 'convertObjectToCypherMapLiteral' with additional parameter --- backend/src/schema/resolvers/groups.js | 2 +- backend/src/schema/resolvers/helpers/Resolver.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 4349ac9bf..6a5bd7966 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -18,7 +18,7 @@ export default { removeUndefinedNullValuesFromObject(matchParams) const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { - const groupMatchParamsCypher = convertObjectToCypherMapLiteral(matchParams) + const groupMatchParamsCypher = convertObjectToCypherMapLiteral(matchParams, true) let groupCypher if (isMember === true) { groupCypher = ` diff --git a/backend/src/schema/resolvers/helpers/Resolver.js b/backend/src/schema/resolvers/helpers/Resolver.js index 56d382690..6e8211521 100644 --- a/backend/src/schema/resolvers/helpers/Resolver.js +++ b/backend/src/schema/resolvers/helpers/Resolver.js @@ -130,15 +130,16 @@ export const removeUndefinedNullValuesFromObject = (obj) => { }) } -export const convertObjectToCypherMapLiteral = (params) => { +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 += index === 0 ? '{' : '' mapLiteral += `${ele[0]}: "${ele[1]}"` mapLiteral += index < paramsEntries.length - 1 ? ', ' : '}' }) + mapLiteral = (addSpaceInfrontIfMapIsNotEmpty && mapLiteral.length > 0 ? ' ' : '') + mapLiteral return mapLiteral }