diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de171cc61..cc4d2deb9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -202,6 +202,8 @@ jobs: run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps neo4j backend - name: backend | Initialize Database run: docker-compose exec -T backend yarn db:migrate init + - name: backend | Migrate Database Up + run: docker-compose exec -T backend yarn db:migrate up - name: backend | Unit test run: docker-compose exec -T backend yarn test ########################################################################## diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index d77363c29..3d698810e 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -302,6 +302,7 @@ export default shield( searchResults: allow, searchPosts: allow, searchUsers: allow, + searchGroups: allow, searchHashtags: allow, embed: allow, Category: allow, diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js index 63279b4bf..3fdf22da3 100644 --- a/backend/src/schema/resolvers/searches.js +++ b/backend/src/schema/resolvers/searches.js @@ -66,6 +66,21 @@ const searchHashtagsSetup = { limit: 'LIMIT $limit', } +const searchGroupsSetup = { + fulltextIndex: 'group_fulltext_search', + match: `MATCH (resource:Group) + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(resource) + WITH user, resource, membership`, + whereClause: `WHERE score >= 0.0 + AND NOT (resource.deleted = true OR resource.disabled = true) + AND (resource.groupType IN ['public', 'closed'] + OR membership.role IN ['usual', 'admin', 'owner'])`, + withClause: 'WITH resource, membership', + returnClause: 'resource { .*, myRole: membership.role, __typename: labels(resource)[0] }', + limit: 'LIMIT $limit', +} + const countSetup = { returnClause: 'toString(size(collect(resource)))', limit: '', @@ -83,6 +98,10 @@ const countHashtagsSetup = { ...searchHashtagsSetup, ...countSetup, } +const countGroupsSetup = { + ...searchGroupsSetup, + ...countSetup, +} const searchResultPromise = async (session, setup, params) => { return session.readTransaction(async (transaction) => { @@ -113,6 +132,7 @@ const multiSearchMap = [ { symbol: '!', setup: searchPostsSetup, resultName: 'posts' }, { symbol: '@', setup: searchUsersSetup, resultName: 'users' }, { symbol: '#', setup: searchHashtagsSetup, resultName: 'hashtags' }, + { symbol: '&', setup: searchGroupsSetup, resultName: 'groups' }, ] export default { @@ -178,13 +198,36 @@ export default { }), } }, + searchGroups: async (_parent, args, context, _resolveInfo) => { + const { query, groupsOffset, firstGroups } = args + let userId = null + if (context.user) userId = context.user.id + return { + groupCount: getSearchResults( + context, + countGroupsSetup, + { + query: queryString(query), + skip: 0, + userId, + }, + countResultCallback, + ), + groups: getSearchResults(context, searchGroupsSetup, { + query: queryString(query), + skip: groupsOffset, + limit: firstGroups, + userId, + }), + } + }, searchResults: async (_parent, args, context, _resolveInfo) => { const { query, limit } = args let userId = null if (context.user) userId = context.user.id - const searchType = query.replace(/^([!@#]?).*$/, '$1') - const searchString = query.replace(/^([!@#])/, '') + const searchType = query.replace(/^([!@#&]?).*$/, '$1') + const searchString = query.replace(/^([!@#&])/, '') const params = { query: queryString(searchString), @@ -197,6 +240,7 @@ export default { return [ ...(await getSearchResults(context, searchPostsSetup, params)), ...(await getSearchResults(context, searchUsersSetup, params)), + ...(await getSearchResults(context, searchGroupsSetup, params)), ...(await getSearchResults(context, searchHashtagsSetup, params)), ] diff --git a/backend/src/schema/types/type/Search.gql b/backend/src/schema/types/type/Search.gql index 9537b5a84..5cb68e22d 100644 --- a/backend/src/schema/types/type/Search.gql +++ b/backend/src/schema/types/type/Search.gql @@ -1,4 +1,4 @@ -union SearchResult = Post | User | Tag +union SearchResult = Post | User | Tag | Group type postSearchResults { postCount: Int @@ -15,9 +15,15 @@ type hashtagSearchResults { hashtags: [Tag]! } +type groupSearchResults { + groupCount: Int + groups: [Group]! +} + type Query { searchPosts(query: String!, firstPosts: Int, postsOffset: Int): postSearchResults! searchUsers(query: String!, firstUsers: Int, usersOffset: Int): userSearchResults! + searchGroups(query: String!, firstGroups: Int, groupsOffset: Int): groupSearchResults! searchHashtags(query: String!, firstHashtags: Int, hashtagsOffset: Int): hashtagSearchResults! searchResults(query: String!, limit: Int = 5): [SearchResult]! } diff --git a/webapp/components/_new/features/SearchResults/SearchResults.spec.js b/webapp/components/_new/features/SearchResults/SearchResults.spec.js index b76226547..19653fcac 100644 --- a/webapp/components/_new/features/SearchResults/SearchResults.spec.js +++ b/webapp/components/_new/features/SearchResults/SearchResults.spec.js @@ -36,6 +36,7 @@ describe('SearchResults', () => { } propsData = { pageSize: 12, + search: '', } wrapper = Wrapper() }) @@ -169,7 +170,7 @@ describe('SearchResults', () => { await wrapper.vm.$nextTick() await expect( wrapper.vm.$options.apollo.searchPosts.variables.bind(wrapper.vm)(), - ).toMatchObject({ query: undefined, firstPosts: 12, postsOffset: 12 }) + ).toMatchObject({ query: '', firstPosts: 12, postsOffset: 12 }) }) it('displays the next page button when next-button is clicked', async () => { @@ -199,7 +200,7 @@ describe('SearchResults', () => { await wrapper.vm.$nextTick() await expect( wrapper.vm.$options.apollo.searchPosts.variables.bind(wrapper.vm)(), - ).toMatchObject({ query: undefined, firstPosts: 12, postsOffset: 24 }) + ).toMatchObject({ query: '', firstPosts: 12, postsOffset: 24 }) }) it('deactivates next page button when next-button is clicked twice', async () => { @@ -234,7 +235,7 @@ describe('SearchResults', () => { await wrapper.vm.$nextTick() await expect( wrapper.vm.$options.apollo.searchPosts.variables.bind(wrapper.vm)(), - ).toMatchObject({ query: undefined, firstPosts: 12, postsOffset: 0 }) + ).toMatchObject({ query: '', firstPosts: 12, postsOffset: 0 }) }) }) }) diff --git a/webapp/components/_new/features/SearchResults/SearchResults.vue b/webapp/components/_new/features/SearchResults/SearchResults.vue index f3730f61d..20385ce64 100644 --- a/webapp/components/_new/features/SearchResults/SearchResults.vue +++ b/webapp/components/_new/features/SearchResults/SearchResults.vue @@ -59,6 +59,14 @@ + +