From fcca0f378d92726f5dc63e0fd0e6672edd210d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 4 Aug 2022 09:24:04 +0200 Subject: [PATCH] Implement 'Group' query, next step --- ...{authentications.ts => authentications.js} | 0 .../src/db/graphql/{groups.ts => groups.js} | 10 +- backend/src/db/graphql/{posts.ts => posts.js} | 0 backend/src/schema/resolvers/groups.js | 34 +- backend/src/schema/resolvers/groups.spec.js | 432 +++++++++++------- backend/src/schema/types/type/Group.gql | 1 + 6 files changed, 307 insertions(+), 170 deletions(-) rename backend/src/db/graphql/{authentications.ts => authentications.js} (100%) rename backend/src/db/graphql/{groups.ts => groups.js} (89%) rename backend/src/db/graphql/{posts.ts => posts.js} (100%) diff --git a/backend/src/db/graphql/authentications.ts b/backend/src/db/graphql/authentications.js similarity index 100% rename from backend/src/db/graphql/authentications.ts rename to backend/src/db/graphql/authentications.js diff --git a/backend/src/db/graphql/groups.ts b/backend/src/db/graphql/groups.js similarity index 89% rename from backend/src/db/graphql/groups.ts rename to backend/src/db/graphql/groups.js index 80b599658..e8da8e90b 100644 --- a/backend/src/db/graphql/groups.ts +++ b/backend/src/db/graphql/groups.js @@ -46,6 +46,7 @@ export const createGroupMutation = gql` export const groupQuery = gql` query ( + $isMember: Boolean $id: ID, $name: String, $slug: String, @@ -55,7 +56,7 @@ export const groupQuery = gql` $description: String, # $groupType: GroupType!, # $actionRadius: GroupActionRadius!, - $categoryIds: [ID] + # $categoryIds: [ID] $locationName: String $first: Int $offset: Int @@ -63,6 +64,7 @@ export const groupQuery = gql` $filter: _GroupFilter ) { Group( + isMember: $isMember id: $id name: $name slug: $slug @@ -72,8 +74,12 @@ export const groupQuery = gql` description: $description # groupType: $groupType # actionRadius: $actionRadius - categoryIds: $categoryIds + # categoryIds: $categoryIds locationName: $locationName + first: $first + offset: $offset + orderBy: $orderBy + filter: $filter ) { id name diff --git a/backend/src/db/graphql/posts.ts b/backend/src/db/graphql/posts.js similarity index 100% rename from backend/src/db/graphql/posts.ts rename to backend/src/db/graphql/posts.js diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 9a55c9f4d..be07fecc6 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -24,18 +24,34 @@ export default { // // params = await maintainPinnedPosts(params) // return neo4jgraphql(object, params, context, resolveInfo) // }, - Group: async (_object, _params, context, _resolveInfo) => { + Group: async (_object, params, context, _resolveInfo) => { + const { isMember } = params const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { - const result = await txc.run( - ` - MATCH (user:User {id: $userId})-[membership:MEMBER_OF]->(group:Group) + let groupCypher + if (isMember === true) { + groupCypher = ` + MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group) RETURN group {.*, myRole: membership.role} - `, - { - userId: context.user.id, - }, - ) + ` + } else { + if (isMember === false) { + groupCypher = ` + MATCH (group:Group) + WHERE NOT (:User {id: $userId})-[:MEMBER_OF]->(group) + RETURN group {.*, myRole: NULL} + ` + } else { + groupCypher = ` + MATCH (group:Group) + OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group) + RETURN group {.*, myRole: membership.role} + ` + } + } + const result = await txc.run(groupCypher, { + userId: context.user.id, + }) const group = result.records.map((record) => record.get('group')) return group }) diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 8860f87f2..58e5f37be 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -1,13 +1,13 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' -import { createGroupMutation } from '../../db/graphql/groups' +import { createGroupMutation, groupQuery } from '../../db/graphql/groups' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' const driver = getDriver() const neode = getNeode() -// Wolle: let query +let query let mutate let authenticatedUser let user @@ -27,7 +27,7 @@ beforeAll(async () => { } }, }) - // Wolle: query = createTestClient(server).query + query = createTestClient(server).query mutate = createTestClient(server).mutate }) @@ -79,162 +79,276 @@ afterEach(async () => { }) describe('Group', () => { - // describe('can be filtered', () => { - // let followedUser, happyPost, cryPost - // beforeEach(async () => { - // ;[followedUser] = await Promise.all([ - // Factory.build( - // 'user', - // { - // id: 'followed-by-me', - // name: 'Followed User', - // }, - // { - // email: 'followed@example.org', - // password: '1234', - // }, - // ), - // ]) - // ;[happyPost, cryPost] = await Promise.all([ - // Factory.build('post', { id: 'happy-post' }, { categoryIds: ['cat4'] }), - // Factory.build('post', { id: 'cry-post' }, { categoryIds: ['cat15'] }), - // Factory.build( - // 'post', - // { - // id: 'post-by-followed-user', - // }, - // { - // categoryIds: ['cat9'], - // author: followedUser, - // }, - // ), - // ]) - // }) - // describe('no filter', () => { - // it('returns all posts', async () => { - // const postQueryNoFilters = gql` - // query Post($filter: _PostFilter) { - // Post(filter: $filter) { - // id - // } - // } - // ` - // const expected = [{ id: 'happy-post' }, { id: 'cry-post' }, { id: 'post-by-followed-user' }] - // variables = { filter: {} } - // await expect(query({ query: postQueryNoFilters, variables })).resolves.toMatchObject({ - // data: { - // Post: expect.arrayContaining(expected), - // }, - // }) - // }) - // }) - // /* it('by categories', async () => { - // const postQueryFilteredByCategories = gql` - // query Post($filter: _PostFilter) { - // Post(filter: $filter) { - // id - // categories { - // id - // } - // } - // } - // ` - // const expected = { - // data: { - // Post: [ - // { - // id: 'post-by-followed-user', - // categories: [{ id: 'cat9' }], - // }, - // ], - // }, - // } - // variables = { ...variables, filter: { categories_some: { id_in: ['cat9'] } } } - // await expect( - // query({ query: postQueryFilteredByCategories, variables }), - // ).resolves.toMatchObject(expected) - // }) */ - // describe('by emotions', () => { - // const postQueryFilteredByEmotions = gql` - // query Post($filter: _PostFilter) { - // Post(filter: $filter) { - // id - // emotions { - // emotion - // } - // } - // } - // ` - // it('filters by single emotion', async () => { - // const expected = { - // data: { - // Post: [ - // { - // id: 'happy-post', - // emotions: [{ emotion: 'happy' }], - // }, - // ], - // }, - // } - // await user.relateTo(happyPost, 'emoted', { emotion: 'happy' }) - // variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy'] } } } - // await expect( - // query({ query: postQueryFilteredByEmotions, variables }), - // ).resolves.toMatchObject(expected) - // }) - // it('filters by multiple emotions', async () => { - // const expected = [ - // { - // id: 'happy-post', - // emotions: [{ emotion: 'happy' }], - // }, - // { - // id: 'cry-post', - // emotions: [{ emotion: 'cry' }], - // }, - // ] - // await user.relateTo(happyPost, 'emoted', { emotion: 'happy' }) - // await user.relateTo(cryPost, 'emoted', { emotion: 'cry' }) - // variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy', 'cry'] } } } - // await expect( - // query({ query: postQueryFilteredByEmotions, variables }), - // ).resolves.toMatchObject({ - // data: { - // Post: expect.arrayContaining(expected), - // }, - // errors: undefined, - // }) - // }) - // }) - // it('by followed-by', async () => { - // const postQueryFilteredByUsersFollowed = gql` - // query Post($filter: _PostFilter) { - // Post(filter: $filter) { - // id - // author { - // id - // name - // } - // } - // } - // ` - // await user.relateTo(followedUser, 'following') - // variables = { filter: { author: { followedBy_some: { id: 'current-user' } } } } - // await expect( - // query({ query: postQueryFilteredByUsersFollowed, variables }), - // ).resolves.toMatchObject({ - // data: { - // Post: [ - // { - // id: 'post-by-followed-user', - // author: { id: 'followed-by-me', name: 'Followed User' }, - // }, - // ], - // }, - // errors: undefined, - // }) - // }) - // }) + describe('unauthenticated', () => { + it('throws authorization error', async () => { + const { errors } = await query({ query: groupQuery, variables: {} }) + expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + let otherUser + + beforeEach(async () => { + otherUser = await Factory.build( + 'user', + { + id: 'other-user', + name: 'Other TestUser', + }, + { + email: 'test2@example.org', + password: '1234', + }, + ) + authenticatedUser = await otherUser.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'others-group', + name: 'Uninteresting Group', + about: 'We will change nothing!', + description: 'We love it like it is!?', + groupType: 'closed', + actionRadius: 'international', + categoryIds, + }, + }) + authenticatedUser = await user.toJson() + await mutate({ + mutation: createGroupMutation, + variables: { + id: 'my-group', + name: 'The Best Group', + about: 'We will change the world!', + description: 'Some description', + groupType: 'public', + actionRadius: 'regional', + categoryIds, + }, + }) + }) + + describe('query can fetch', () => { + it('groups where user is member (or owner in this case)', async () => { + const expected = { + data: { + Group: [ + { + id: 'my-group', + slug: 'the-best-group', + myRole: 'owner', + }, + ], + }, + errors: undefined, + } + await expect( + query({ query: groupQuery, variables: { isMember: true } }), + ).resolves.toMatchObject(expected) + }) + + it('groups where user is not(!) member', async () => { + const expected = { + data: { + Group: expect.arrayContaining([ + expect.objectContaining({ + id: 'others-group', + slug: 'uninteresting-group', + myRole: null, + }), + ]), + }, + errors: undefined, + } + await expect( + query({ query: groupQuery, variables: { isMember: false } }), + ).resolves.toMatchObject(expected) + }) + + it('all groups', async () => { + const expected = { + data: { + Group: expect.arrayContaining([ + expect.objectContaining({ + id: 'my-group', + slug: 'the-best-group', + myRole: 'owner', + }), + expect.objectContaining({ + id: 'others-group', + slug: 'uninteresting-group', + myRole: null, + }), + ]), + }, + errors: undefined, + } + await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject(expected) + }) + }) + + // Wolle: describe('can be filtered', () => { + // let followedUser, happyPost, cryPost + // beforeEach(async () => { + // ;[followedUser] = await Promise.all([ + // Factory.build( + // 'user', + // { + // id: 'followed-by-me', + // name: 'Followed User', + // }, + // { + // email: 'followed@example.org', + // password: '1234', + // }, + // ), + // ]) + // ;[happyPost, cryPost] = await Promise.all([ + // Factory.build('post', { id: 'happy-post' }, { categoryIds: ['cat4'] }), + // Factory.build('post', { id: 'cry-post' }, { categoryIds: ['cat15'] }), + // Factory.build( + // 'post', + // { + // id: 'post-by-followed-user', + // }, + // { + // categoryIds: ['cat9'], + // author: followedUser, + // }, + // ), + // ]) + // }) + // describe('no filter', () => { + // it('returns all posts', async () => { + // const postQueryNoFilters = gql` + // query Post($filter: _PostFilter) { + // Post(filter: $filter) { + // id + // } + // } + // ` + // const expected = [{ id: 'happy-post' }, { id: 'cry-post' }, { id: 'post-by-followed-user' }] + // variables = { filter: {} } + // await expect(query({ query: postQueryNoFilters, variables })).resolves.toMatchObject({ + // data: { + // Post: expect.arrayContaining(expected), + // }, + // }) + // }) + // }) + // /* it('by categories', async () => { + // const postQueryFilteredByCategories = gql` + // query Post($filter: _PostFilter) { + // Post(filter: $filter) { + // id + // categories { + // id + // } + // } + // } + // ` + // const expected = { + // data: { + // Post: [ + // { + // id: 'post-by-followed-user', + // categories: [{ id: 'cat9' }], + // }, + // ], + // }, + // } + // variables = { ...variables, filter: { categories_some: { id_in: ['cat9'] } } } + // await expect( + // query({ query: postQueryFilteredByCategories, variables }), + // ).resolves.toMatchObject(expected) + // }) */ + // describe('by emotions', () => { + // const postQueryFilteredByEmotions = gql` + // query Post($filter: _PostFilter) { + // Post(filter: $filter) { + // id + // emotions { + // emotion + // } + // } + // } + // ` + // it('filters by single emotion', async () => { + // const expected = { + // data: { + // Post: [ + // { + // id: 'happy-post', + // emotions: [{ emotion: 'happy' }], + // }, + // ], + // }, + // } + // await user.relateTo(happyPost, 'emoted', { emotion: 'happy' }) + // variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy'] } } } + // await expect( + // query({ query: postQueryFilteredByEmotions, variables }), + // ).resolves.toMatchObject(expected) + // }) + // it('filters by multiple emotions', async () => { + // const expected = [ + // { + // id: 'happy-post', + // emotions: [{ emotion: 'happy' }], + // }, + // { + // id: 'cry-post', + // emotions: [{ emotion: 'cry' }], + // }, + // ] + // await user.relateTo(happyPost, 'emoted', { emotion: 'happy' }) + // await user.relateTo(cryPost, 'emoted', { emotion: 'cry' }) + // variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy', 'cry'] } } } + // await expect( + // query({ query: postQueryFilteredByEmotions, variables }), + // ).resolves.toMatchObject({ + // data: { + // Post: expect.arrayContaining(expected), + // }, + // errors: undefined, + // }) + // }) + // }) + // it('by followed-by', async () => { + // const postQueryFilteredByUsersFollowed = gql` + // query Post($filter: _PostFilter) { + // Post(filter: $filter) { + // id + // author { + // id + // name + // } + // } + // } + // ` + // await user.relateTo(followedUser, 'following') + // variables = { filter: { author: { followedBy_some: { id: 'current-user' } } } } + // await expect( + // query({ query: postQueryFilteredByUsersFollowed, variables }), + // ).resolves.toMatchObject({ + // data: { + // Post: [ + // { + // id: 'post-by-followed-user', + // author: { id: 'followed-by-me', name: 'Followed User' }, + // }, + // ], + // }, + // errors: undefined, + // }) + // }) + // }) + }) }) describe('CreateGroup', () => { diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index cd15689ec..b8e00f0ee 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -170,6 +170,7 @@ input _GroupFilter { type Query { Group( + isMember: Boolean # if 'undefined' or 'null' then all groups id: ID name: String slug: String