mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-04-03 08:05:33 +00:00
refactor(backend): improve map performance (#9476)
This commit is contained in:
parent
b07830769d
commit
2cd07950c6
@ -27,7 +27,7 @@ export default {
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 92,
|
||||
lines: 93,
|
||||
},
|
||||
},
|
||||
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
query GroupWithLocationFilter($hasLocation: Boolean) {
|
||||
Group(hasLocation: $hasLocation) {
|
||||
id
|
||||
location {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
query PostWithLocationFilter($filter: _PostFilter) {
|
||||
Post(filter: $filter) {
|
||||
id
|
||||
eventLocation {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
query UserWithLocationFilter($filter: _UserFilter) {
|
||||
User(filter: $filter) {
|
||||
id
|
||||
location {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -23,7 +23,7 @@ import type { Context } from '@src/context'
|
||||
export default {
|
||||
Query: {
|
||||
Group: async (_object, params, context: Context, _resolveInfo) => {
|
||||
const { isMember, id, slug, first, offset } = params
|
||||
const { isMember, hasLocation, id, slug, first, offset } = params
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
return await session.readTransaction(async (txc) => {
|
||||
@ -35,10 +35,13 @@ export default {
|
||||
if (slug !== undefined) matchFilters.push('group.slug = $slug')
|
||||
const matchWhere = matchFilters.length ? `WHERE ${matchFilters.join(' AND ')}` : ''
|
||||
|
||||
const locationMatch = hasLocation === true ? 'MATCH (group)-[:IS_IN]->(:Location)' : ''
|
||||
|
||||
const transactionResponse = await txc.run(
|
||||
`
|
||||
MATCH (group:Group)
|
||||
${matchWhere}
|
||||
${locationMatch}
|
||||
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||
WITH group, membership
|
||||
${(isMember === true && "WHERE membership IS NOT NULL AND (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])") || ''}
|
||||
|
||||
55
backend/src/graphql/resolvers/helpers/Resolver.spec.ts
Normal file
55
backend/src/graphql/resolvers/helpers/Resolver.spec.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { removeUndefinedNullValuesFromObject, convertObjectToCypherMapLiteral } from './Resolver'
|
||||
|
||||
describe('removeUndefinedNullValuesFromObject', () => {
|
||||
it('removes undefined values', () => {
|
||||
const obj = { a: 1, b: undefined, c: 'hello' }
|
||||
removeUndefinedNullValuesFromObject(obj)
|
||||
expect(obj).toEqual({ a: 1, c: 'hello' })
|
||||
})
|
||||
|
||||
it('removes null values', () => {
|
||||
const obj = { a: 1, b: null, c: 'hello' }
|
||||
removeUndefinedNullValuesFromObject(obj)
|
||||
expect(obj).toEqual({ a: 1, c: 'hello' })
|
||||
})
|
||||
|
||||
it('keeps falsy but defined values', () => {
|
||||
const obj = { a: 0, b: false, c: '' }
|
||||
removeUndefinedNullValuesFromObject(obj)
|
||||
expect(obj).toEqual({ a: 0, b: false, c: '' })
|
||||
})
|
||||
|
||||
it('handles empty object', () => {
|
||||
const obj = {}
|
||||
removeUndefinedNullValuesFromObject(obj)
|
||||
expect(obj).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('convertObjectToCypherMapLiteral', () => {
|
||||
it('converts single entry', () => {
|
||||
expect(convertObjectToCypherMapLiteral({ id: 'g0' })).toBe('{id: "g0"}')
|
||||
})
|
||||
|
||||
it('converts multiple entries', () => {
|
||||
expect(convertObjectToCypherMapLiteral({ id: 'g0', slug: 'yoga' })).toBe(
|
||||
'{id: "g0", slug: "yoga"}',
|
||||
)
|
||||
})
|
||||
|
||||
it('returns empty string for empty object', () => {
|
||||
expect(convertObjectToCypherMapLiteral({})).toBe('')
|
||||
})
|
||||
|
||||
it('adds space in front when addSpaceInfrontIfMapIsNotEmpty is true and map is not empty', () => {
|
||||
expect(convertObjectToCypherMapLiteral({ id: 'g0' }, true)).toBe(' {id: "g0"}')
|
||||
})
|
||||
|
||||
it('does not add space when addSpaceInfrontIfMapIsNotEmpty is true but map is empty', () => {
|
||||
expect(convertObjectToCypherMapLiteral({}, true)).toBe('')
|
||||
})
|
||||
|
||||
it('does not add space when addSpaceInfrontIfMapIsNotEmpty is false', () => {
|
||||
expect(convertObjectToCypherMapLiteral({ id: 'g0' }, false)).toBe('{id: "g0"}')
|
||||
})
|
||||
})
|
||||
176
backend/src/graphql/resolvers/helpers/filterHasLocation.spec.ts
Normal file
176
backend/src/graphql/resolvers/helpers/filterHasLocation.spec.ts
Normal file
@ -0,0 +1,176 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import GroupWithLocationFilter from '@graphql/queries/groups/GroupWithLocationFilter.gql'
|
||||
import PostWithLocationFilter from '@graphql/queries/posts/PostWithLocationFilter.gql'
|
||||
import UserWithLocationFilter from '@graphql/queries/users/UserWithLocationFilter.gql'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import type { Context } from '@src/context'
|
||||
|
||||
let authenticatedUser: Context['user']
|
||||
const context = () => ({ authenticatedUser })
|
||||
let query: ApolloTestSetup['query']
|
||||
let database: ApolloTestSetup['database']
|
||||
let server: ApolloTestSetup['server']
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
const apolloSetup = await createApolloTestSetup({ context })
|
||||
query = apolloSetup.query
|
||||
database = apolloSetup.database
|
||||
server = apolloSetup.server
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
describe('hasLocation filter', () => {
|
||||
describe('User', () => {
|
||||
beforeEach(async () => {
|
||||
const location = await Factory.build('location', {
|
||||
id: 'loc-hamburg',
|
||||
name: 'Hamburg',
|
||||
type: 'region',
|
||||
lng: 10.0,
|
||||
lat: 53.55,
|
||||
})
|
||||
const userWithLocation = await Factory.build('user', {
|
||||
id: 'u-with-loc',
|
||||
name: 'User With Location',
|
||||
})
|
||||
await userWithLocation.relateTo(location, 'isIn')
|
||||
await Factory.build('user', {
|
||||
id: 'u-without-loc',
|
||||
name: 'User Without Location',
|
||||
})
|
||||
authenticatedUser = await userWithLocation.toJson()
|
||||
})
|
||||
|
||||
it('returns all users without filter', async () => {
|
||||
const result = await query({ query: UserWithLocationFilter })
|
||||
expect(result.data?.User.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('returns only users with location when hasLocation is true', async () => {
|
||||
const result = await query({
|
||||
query: UserWithLocationFilter,
|
||||
variables: { filter: { hasLocation: true } },
|
||||
})
|
||||
const ids = result.data?.User.map((u: { id: string }) => u.id)
|
||||
expect(ids).toContain('u-with-loc')
|
||||
expect(ids).not.toContain('u-without-loc')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Group', () => {
|
||||
beforeEach(async () => {
|
||||
const location = await Factory.build('location', {
|
||||
id: 'loc-berlin',
|
||||
name: 'Berlin',
|
||||
type: 'region',
|
||||
lng: 13.4,
|
||||
lat: 52.52,
|
||||
})
|
||||
const owner = await Factory.build('user', { id: 'group-owner', name: 'Owner' })
|
||||
authenticatedUser = await owner.toJson()
|
||||
|
||||
const groupWithLocation = await Factory.build('group', {
|
||||
id: 'g-with-loc',
|
||||
name: 'Group With Location',
|
||||
groupType: 'public',
|
||||
ownerId: 'group-owner',
|
||||
})
|
||||
await groupWithLocation.relateTo(location, 'isIn')
|
||||
|
||||
await Factory.build('group', {
|
||||
id: 'g-without-loc',
|
||||
name: 'Group Without Location',
|
||||
groupType: 'public',
|
||||
ownerId: 'group-owner',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns all groups without filter', async () => {
|
||||
const result = await query({ query: GroupWithLocationFilter })
|
||||
expect(result.data?.Group.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('returns only groups with location when hasLocation is true', async () => {
|
||||
const result = await query({
|
||||
query: GroupWithLocationFilter,
|
||||
variables: { hasLocation: true },
|
||||
})
|
||||
const ids = result.data?.Group.map((g: { id: string }) => g.id)
|
||||
expect(ids).toContain('g-with-loc')
|
||||
expect(ids).not.toContain('g-without-loc')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Post', () => {
|
||||
beforeEach(async () => {
|
||||
const author = await Factory.build('user', { id: 'post-author', name: 'Author' })
|
||||
authenticatedUser = await author.toJson()
|
||||
|
||||
await Factory.build('location', {
|
||||
id: 'loc-munich',
|
||||
name: 'Munich',
|
||||
type: 'region',
|
||||
lng: 11.58,
|
||||
lat: 48.14,
|
||||
})
|
||||
await Factory.build('post', {
|
||||
id: 'p-with-loc',
|
||||
title: 'Event With Location',
|
||||
postType: 'Event',
|
||||
authorId: 'post-author',
|
||||
})
|
||||
// Post model has no isIn relationship defined in Neode, use Cypher directly
|
||||
const session = database.driver.session()
|
||||
try {
|
||||
await session.writeTransaction((txc) =>
|
||||
txc.run(`
|
||||
MATCH (p:Post {id: 'p-with-loc'}), (l:Location {id: 'loc-munich'})
|
||||
MERGE (p)-[:IS_IN]->(l)
|
||||
`),
|
||||
)
|
||||
} finally {
|
||||
await session.close()
|
||||
}
|
||||
|
||||
await Factory.build('post', {
|
||||
id: 'p-without-loc',
|
||||
title: 'Event Without Location',
|
||||
postType: 'Event',
|
||||
authorId: 'post-author',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns all posts without hasLocation filter', async () => {
|
||||
const result = await query({
|
||||
query: PostWithLocationFilter,
|
||||
})
|
||||
expect(result.data?.Post.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('returns only posts with location when hasLocation is true', async () => {
|
||||
const result = await query({
|
||||
query: PostWithLocationFilter,
|
||||
variables: { filter: { hasLocation: true } },
|
||||
})
|
||||
const ids = result.data?.Post.map((p: { id: string }) => p.id)
|
||||
expect(ids).toContain('p-with-loc')
|
||||
expect(ids).not.toContain('p-without-loc')
|
||||
})
|
||||
})
|
||||
})
|
||||
56
backend/src/graphql/resolvers/helpers/filterHasLocation.ts
Normal file
56
backend/src/graphql/resolvers/helpers/filterHasLocation.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import type { Context } from '@src/context'
|
||||
|
||||
interface FilterParams {
|
||||
filter?: {
|
||||
hasLocation?: boolean
|
||||
id_in?: string[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type AllowedLabel = 'User' | 'Post'
|
||||
|
||||
const getIdsWithLocation = async (context: Context, label: AllowedLabel): Promise<string[]> => {
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const result = await session.readTransaction(async (transaction) => {
|
||||
const cypher = `
|
||||
MATCH (n:${label})-[:IS_IN]->(l:Location)
|
||||
RETURN collect(n.id) AS ids`
|
||||
const response = await transaction.run(cypher)
|
||||
return response.records.map((record) => record.get('ids') as string[])
|
||||
})
|
||||
const [ids] = result
|
||||
return ids
|
||||
} finally {
|
||||
await session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const mergeIdIn = (existing: string[] | undefined, incoming: string[]): string[] => {
|
||||
if (!existing) return incoming
|
||||
return existing.filter((id) => incoming.includes(id))
|
||||
}
|
||||
|
||||
export const filterUsersHasLocation = async (
|
||||
params: FilterParams,
|
||||
context: Context,
|
||||
): Promise<FilterParams> => {
|
||||
if (!params.filter?.hasLocation) return params
|
||||
delete params.filter.hasLocation
|
||||
const userIds = await getIdsWithLocation(context, 'User')
|
||||
params.filter.id_in = mergeIdIn(params.filter.id_in, userIds)
|
||||
return params
|
||||
}
|
||||
|
||||
export const filterPostsHasLocation = async (
|
||||
params: FilterParams,
|
||||
context: Context,
|
||||
): Promise<FilterParams> => {
|
||||
if (!params.filter?.hasLocation) return params
|
||||
delete params.filter.hasLocation
|
||||
const postIds = await getIdsWithLocation(context, 'Post')
|
||||
params.filter.id_in = mergeIdIn(params.filter.id_in, postIds)
|
||||
return params
|
||||
}
|
||||
207
backend/src/graphql/resolvers/helpers/filterHelpers.spec.ts
Normal file
207
backend/src/graphql/resolvers/helpers/filterHelpers.spec.ts
Normal file
@ -0,0 +1,207 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import CreatePost from '@graphql/queries/posts/CreatePost.gql'
|
||||
import Post from '@graphql/queries/posts/Post.gql'
|
||||
import { createApolloTestSetup } from '@root/test/helpers'
|
||||
|
||||
import type { ApolloTestSetup } from '@root/test/helpers'
|
||||
import type { Context } from '@src/context'
|
||||
|
||||
let authenticatedUser: Context['user']
|
||||
const context = () => ({ authenticatedUser })
|
||||
let query: ApolloTestSetup['query']
|
||||
let mutate: ApolloTestSetup['mutate']
|
||||
let database: ApolloTestSetup['database']
|
||||
let server: ApolloTestSetup['server']
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
const apolloSetup = await createApolloTestSetup({ context })
|
||||
query = apolloSetup.query
|
||||
mutate = apolloSetup.mutate
|
||||
database = apolloSetup.database
|
||||
server = apolloSetup.server
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
describe('filterForMutedUsers', () => {
|
||||
describe('when looking up a single post by id', () => {
|
||||
it('does not filter muted users', async () => {
|
||||
const author = await Factory.build('user', { id: 'muted-author', name: 'Muted Author' })
|
||||
const viewer = await Factory.build('user', { id: 'viewer', name: 'Viewer' })
|
||||
|
||||
// Viewer mutes author
|
||||
const session = database.driver.session()
|
||||
try {
|
||||
await session.writeTransaction((txc) =>
|
||||
txc.run(`
|
||||
MATCH (viewer:User {id: 'viewer'}), (author:User {id: 'muted-author'})
|
||||
MERGE (viewer)-[:MUTED]->(author)
|
||||
`),
|
||||
)
|
||||
} finally {
|
||||
await session.close()
|
||||
}
|
||||
|
||||
// Author creates a post
|
||||
authenticatedUser = await author.toJson()
|
||||
await mutate({
|
||||
mutation: CreatePost,
|
||||
variables: {
|
||||
id: 'muted-post',
|
||||
title: 'Post by muted user',
|
||||
content: 'Some content here for the post',
|
||||
postType: 'Article',
|
||||
},
|
||||
})
|
||||
|
||||
// Viewer queries for the specific post by id — should still see it
|
||||
authenticatedUser = await viewer.toJson()
|
||||
const result = await query({
|
||||
query: Post,
|
||||
variables: { id: 'muted-post' },
|
||||
})
|
||||
expect(result.data?.Post).toHaveLength(1)
|
||||
expect(result.data?.Post[0].id).toBe('muted-post')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when listing all posts', () => {
|
||||
it('filters posts from muted users', async () => {
|
||||
const author = await Factory.build('user', { id: 'muted-author', name: 'Muted Author' })
|
||||
const otherAuthor = await Factory.build('user', {
|
||||
id: 'other-author',
|
||||
name: 'Other Author',
|
||||
})
|
||||
const viewer = await Factory.build('user', { id: 'viewer', name: 'Viewer' })
|
||||
|
||||
// Viewer mutes author
|
||||
const session = database.driver.session()
|
||||
try {
|
||||
await session.writeTransaction((txc) =>
|
||||
txc.run(`
|
||||
MATCH (viewer:User {id: 'viewer'}), (author:User {id: 'muted-author'})
|
||||
MERGE (viewer)-[:MUTED]->(author)
|
||||
`),
|
||||
)
|
||||
} finally {
|
||||
await session.close()
|
||||
}
|
||||
|
||||
// Both authors create posts
|
||||
authenticatedUser = await author.toJson()
|
||||
await mutate({
|
||||
mutation: CreatePost,
|
||||
variables: {
|
||||
id: 'muted-post',
|
||||
title: 'Post by muted user',
|
||||
content: 'Some content here for the muted post',
|
||||
postType: 'Article',
|
||||
},
|
||||
})
|
||||
|
||||
authenticatedUser = await otherAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: CreatePost,
|
||||
variables: {
|
||||
id: 'visible-post',
|
||||
title: 'Post by other user',
|
||||
content: 'Some content here for the visible post',
|
||||
postType: 'Article',
|
||||
},
|
||||
})
|
||||
|
||||
// Viewer lists all posts — should not see muted author's post
|
||||
authenticatedUser = await viewer.toJson()
|
||||
const result = await query({ query: Post })
|
||||
const ids = result.data?.Post.map((p: { id: string }) => p.id)
|
||||
expect(ids).toContain('visible-post')
|
||||
expect(ids).not.toContain('muted-post')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('filterPostsOfMyGroups', () => {
|
||||
describe('when user has no group memberships', () => {
|
||||
it('returns empty for postsInMyGroups filter', async () => {
|
||||
const author = await Factory.build('user', { id: 'author', name: 'Author' })
|
||||
authenticatedUser = await author.toJson()
|
||||
|
||||
await mutate({
|
||||
mutation: CreatePost,
|
||||
variables: {
|
||||
id: 'some-post',
|
||||
title: 'A regular post',
|
||||
content: 'Some content here for the post',
|
||||
postType: 'Article',
|
||||
},
|
||||
})
|
||||
|
||||
// Query with postsInMyGroups but user has no groups
|
||||
const result = await query({
|
||||
query: Post,
|
||||
variables: { filter: { postsInMyGroups: true } },
|
||||
})
|
||||
expect(result.data?.Post).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('filterInvisiblePosts', () => {
|
||||
describe('with closed group posts', () => {
|
||||
beforeEach(async () => {
|
||||
const owner = await Factory.build('user', { id: 'group-owner', name: 'Group Owner' })
|
||||
await Factory.build('user', { id: 'outsider', name: 'Outsider' })
|
||||
|
||||
authenticatedUser = await owner.toJson()
|
||||
await Factory.build('group', {
|
||||
id: 'closed-group',
|
||||
name: 'Closed Group',
|
||||
groupType: 'closed',
|
||||
ownerId: 'group-owner',
|
||||
})
|
||||
|
||||
await mutate({
|
||||
mutation: CreatePost,
|
||||
variables: {
|
||||
id: 'closed-group-post',
|
||||
title: 'Secret Post',
|
||||
content: 'Some content here for the secret post',
|
||||
postType: 'Article',
|
||||
groupId: 'closed-group',
|
||||
},
|
||||
})
|
||||
|
||||
await mutate({
|
||||
mutation: CreatePost,
|
||||
variables: {
|
||||
id: 'public-post',
|
||||
title: 'Public Post',
|
||||
content: 'Some content here for the public post',
|
||||
postType: 'Article',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('filters posts in non-public groups for non-members', async () => {
|
||||
const outsider = await database.neode.find('User', 'outsider')
|
||||
authenticatedUser = (await outsider.toJson()) as Context['user']
|
||||
const result = await query({ query: Post })
|
||||
const ids = result.data?.Post.map((p: { id: string }) => p.id)
|
||||
expect(ids).toContain('public-post')
|
||||
expect(ids).not.toContain('closed-group-post')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -13,6 +13,7 @@ import { UserInputError } from '@graphql/errors'
|
||||
|
||||
import { validateEventParams } from './helpers/events'
|
||||
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
||||
import { filterPostsHasLocation } from './helpers/filterHasLocation'
|
||||
import { filterInvisiblePosts } from './helpers/filterInvisiblePosts'
|
||||
import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups'
|
||||
import Resolver from './helpers/Resolver'
|
||||
@ -61,6 +62,7 @@ export default {
|
||||
params = await filterInvisiblePosts(params, context)
|
||||
params = await filterForMutedUsers(params, context)
|
||||
params = filterEventDates(params)
|
||||
params = await filterPostsHasLocation(params, context)
|
||||
params = await maintainPinnedPosts(params)
|
||||
return neo4jgraphql(object, params, context, resolveInfo)
|
||||
},
|
||||
@ -68,6 +70,7 @@ export default {
|
||||
params = await filterPostsOfMyGroups(params, context)
|
||||
params = await filterInvisiblePosts(params, context)
|
||||
params = await filterForMutedUsers(params, context)
|
||||
params = await filterPostsHasLocation(params, context)
|
||||
params = await maintainGroupPinnedPosts(params)
|
||||
return neo4jgraphql(object, params, context, resolveInfo)
|
||||
},
|
||||
|
||||
@ -12,6 +12,7 @@ import { getNeode } from '@db/neo4j'
|
||||
import { UserInputError, ForbiddenError } from '@graphql/errors'
|
||||
|
||||
import { defaultTrophyBadge, defaultVerificationBadge } from './badges'
|
||||
import { filterUsersHasLocation } from './helpers/filterHasLocation'
|
||||
import normalizeEmail from './helpers/normalizeEmail'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import { images } from './images/images'
|
||||
@ -66,6 +67,7 @@ export default {
|
||||
await session.close()
|
||||
}
|
||||
}
|
||||
args = await filterUsersHasLocation(args, context)
|
||||
return neo4jgraphql(object, args, context, resolveInfo)
|
||||
},
|
||||
},
|
||||
|
||||
@ -79,6 +79,7 @@ input _GroupFilter {
|
||||
type Query {
|
||||
Group(
|
||||
isMember: Boolean # if 'undefined' or 'null' then get all groups
|
||||
hasLocation: Boolean # if 'true' then only groups with a location
|
||||
id: ID
|
||||
slug: String
|
||||
first: Int
|
||||
|
||||
@ -92,6 +92,7 @@ input _PostFilter {
|
||||
postType_in: [PostType]
|
||||
eventStart_gte: String
|
||||
eventEnd_gte: String
|
||||
hasLocation: Boolean
|
||||
}
|
||||
|
||||
enum _PostOrdering {
|
||||
|
||||
@ -158,6 +158,7 @@ type User {
|
||||
input _UserFilter {
|
||||
AND: [_UserFilter!]
|
||||
OR: [_UserFilter!]
|
||||
hasLocation: Boolean
|
||||
name_contains: String
|
||||
about_contains: String
|
||||
slug_contains: String
|
||||
|
||||
51
webapp/graphql/MapQuery.js
Normal file
51
webapp/graphql/MapQuery.js
Normal file
@ -0,0 +1,51 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const mapQuery = (i18n) => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
query MapData($userFilter: _UserFilter, $groupHasLocation: Boolean, $postFilter: _PostFilter) {
|
||||
User(filter: $userFilter) {
|
||||
id
|
||||
slug
|
||||
name
|
||||
about
|
||||
location {
|
||||
id
|
||||
name(lang: "${lang}")
|
||||
lng
|
||||
lat
|
||||
}
|
||||
}
|
||||
Group(hasLocation: $groupHasLocation) {
|
||||
id
|
||||
slug
|
||||
name
|
||||
about
|
||||
location {
|
||||
id
|
||||
name(lang: "${lang}")
|
||||
lng
|
||||
lat
|
||||
}
|
||||
}
|
||||
Post(filter: $postFilter) {
|
||||
id
|
||||
slug
|
||||
title
|
||||
content
|
||||
postType
|
||||
eventStart
|
||||
eventEnd
|
||||
eventVenue
|
||||
eventLocationName
|
||||
eventIsOnline
|
||||
eventLocation {
|
||||
id
|
||||
name(lang: "${lang}")
|
||||
lng
|
||||
lat
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
@ -64,9 +64,8 @@ import mapboxgl from 'mapbox-gl'
|
||||
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'
|
||||
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { profileUserQuery, mapUserQuery } from '~/graphql/User'
|
||||
import { groupQuery } from '~/graphql/groups'
|
||||
import { filterMapPosts } from '~/graphql/PostQuery.js'
|
||||
import { profileUserQuery } from '~/graphql/User'
|
||||
import { mapQuery } from '~/graphql/MapQuery'
|
||||
import mobile from '~/mixins/mobile'
|
||||
import Empty from '~/components/Empty/Empty'
|
||||
import MapStylesButtons from '~/components/Map/MapStylesButtons'
|
||||
@ -363,7 +362,7 @@ export default {
|
||||
if (this.isPreparedForMarkers) {
|
||||
// add markers for "users"
|
||||
this.users.forEach((user) => {
|
||||
if (user.id !== this.currentUser.id && user.location) {
|
||||
if (user.id !== this.currentUser.id) {
|
||||
this.markers.geoJSON.push({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
@ -403,45 +402,41 @@ export default {
|
||||
}
|
||||
// add markers for "groups"
|
||||
this.groups.forEach((group) => {
|
||||
if (group.location) {
|
||||
this.markers.geoJSON.push({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
type: 'group',
|
||||
iconName: 'marker-red',
|
||||
iconRotate: 0.0,
|
||||
id: group.id,
|
||||
slug: group.slug,
|
||||
name: group.name,
|
||||
description: group.about ? group.about : undefined,
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: this.getCoordinates(group.location),
|
||||
},
|
||||
})
|
||||
}
|
||||
this.markers.geoJSON.push({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
type: 'group',
|
||||
iconName: 'marker-red',
|
||||
iconRotate: 0.0,
|
||||
id: group.id,
|
||||
slug: group.slug,
|
||||
name: group.name,
|
||||
description: group.about ? group.about : undefined,
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: this.getCoordinates(group.location),
|
||||
},
|
||||
})
|
||||
})
|
||||
// add markers for "posts", post type "Event" with location coordinates
|
||||
this.posts.forEach((post) => {
|
||||
if (post.postType.includes('Event') && post.eventLocation) {
|
||||
this.markers.geoJSON.push({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
type: 'event',
|
||||
iconName: 'marker-purple',
|
||||
iconRotate: 0.0,
|
||||
id: post.id,
|
||||
slug: post.slug,
|
||||
name: post.title,
|
||||
description: this.$filters.removeHtml(post.content),
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: this.getCoordinates(post.eventLocation),
|
||||
},
|
||||
})
|
||||
}
|
||||
this.markers.geoJSON.push({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
type: 'event',
|
||||
iconName: 'marker-purple',
|
||||
iconRotate: 0.0,
|
||||
id: post.id,
|
||||
slug: post.slug,
|
||||
name: post.title,
|
||||
description: this.$filters.removeHtml(post.content),
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: this.getCoordinates(post.eventLocation),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
this.markers.isGeoJSON = true
|
||||
@ -514,47 +509,26 @@ export default {
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
User: {
|
||||
mapData: {
|
||||
query() {
|
||||
return mapUserQuery(this.$i18n)
|
||||
},
|
||||
variables() {
|
||||
return {}
|
||||
},
|
||||
update({ User }) {
|
||||
this.users = User
|
||||
this.addMarkersOnCheckPrepared()
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
Group: {
|
||||
query() {
|
||||
return groupQuery(this.$i18n)
|
||||
},
|
||||
variables() {
|
||||
return {}
|
||||
},
|
||||
update({ Group }) {
|
||||
this.groups = Group
|
||||
this.addMarkersOnCheckPrepared()
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
Post: {
|
||||
query() {
|
||||
return filterMapPosts(this.$i18n)
|
||||
return mapQuery(this.$i18n)
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
filter: {
|
||||
userFilter: { hasLocation: true },
|
||||
groupHasLocation: true,
|
||||
postFilter: {
|
||||
postType_in: ['Event'],
|
||||
eventStart_gte: new Date(),
|
||||
// would be good to just query for events with defined "eventLocation". couldn't get it working
|
||||
hasLocation: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
update({ Post }) {
|
||||
update({ User, Group, Post }) {
|
||||
this.users = User
|
||||
this.groups = Group
|
||||
this.posts = Post
|
||||
this.addMarkersOnCheckPrepared()
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user