mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #730 from Human-Connection/269-filter_by_followed_users-backend_part
269 filter by followed users backend part
This commit is contained in:
commit
cc4db62a14
12
backend/src/middleware/filterBubble/filterBubble.js
Normal file
12
backend/src/middleware/filterBubble/filterBubble.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import replaceParams from './replaceParams'
|
||||||
|
|
||||||
|
const replaceFilterBubbleParams = async (resolve, root, args, context, resolveInfo) => {
|
||||||
|
args = await replaceParams(args, context)
|
||||||
|
return resolve(root, args, context, resolveInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Query: {
|
||||||
|
Post: replaceFilterBubbleParams,
|
||||||
|
},
|
||||||
|
}
|
||||||
76
backend/src/middleware/filterBubble/filterBubble.spec.js
Normal file
76
backend/src/middleware/filterBubble/filterBubble.spec.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { GraphQLClient } from 'graphql-request'
|
||||||
|
import { host, login } from '../../jest/helpers'
|
||||||
|
import Factory from '../../seed/factories'
|
||||||
|
|
||||||
|
const factory = Factory()
|
||||||
|
|
||||||
|
const currentUserParams = {
|
||||||
|
email: 'you@example.org',
|
||||||
|
name: 'This is you',
|
||||||
|
password: '1234',
|
||||||
|
}
|
||||||
|
const followedAuthorParams = {
|
||||||
|
id: 'u2',
|
||||||
|
email: 'followed@example.org',
|
||||||
|
name: 'Followed User',
|
||||||
|
password: '1234',
|
||||||
|
}
|
||||||
|
const randomAuthorParams = {
|
||||||
|
email: 'someone@example.org',
|
||||||
|
name: 'Someone else',
|
||||||
|
password: 'else',
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
factory.create('User', currentUserParams),
|
||||||
|
factory.create('User', followedAuthorParams),
|
||||||
|
factory.create('User', randomAuthorParams),
|
||||||
|
])
|
||||||
|
const [asYourself, asFollowedUser, asSomeoneElse] = await Promise.all([
|
||||||
|
Factory().authenticateAs(currentUserParams),
|
||||||
|
Factory().authenticateAs(followedAuthorParams),
|
||||||
|
Factory().authenticateAs(randomAuthorParams),
|
||||||
|
])
|
||||||
|
await asYourself.follow({ id: 'u2', type: 'User' })
|
||||||
|
await asFollowedUser.create('Post', { title: 'This is the post of a followed user' })
|
||||||
|
await asSomeoneElse.create('Post', { title: 'This is some random post' })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('FilterBubble middleware', () => {
|
||||||
|
describe('given an authenticated user', () => {
|
||||||
|
let authenticatedClient
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const headers = await login(currentUserParams)
|
||||||
|
authenticatedClient = new GraphQLClient(host, { headers })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('no filter bubble', () => {
|
||||||
|
it('returns all posts', async () => {
|
||||||
|
const query = '{ Post( filterBubble: {}) { title } }'
|
||||||
|
const expected = {
|
||||||
|
Post: [
|
||||||
|
{ title: 'This is some random post' },
|
||||||
|
{ title: 'This is the post of a followed user' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
await expect(authenticatedClient.request(query)).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('filtering for posts of followed users only', () => {
|
||||||
|
it('returns only posts authored by followed users', async () => {
|
||||||
|
const query = '{ Post( filterBubble: { author: following }) { title } }'
|
||||||
|
const expected = {
|
||||||
|
Post: [{ title: 'This is the post of a followed user' }],
|
||||||
|
}
|
||||||
|
await expect(authenticatedClient.request(query)).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
31
backend/src/middleware/filterBubble/replaceParams.js
Normal file
31
backend/src/middleware/filterBubble/replaceParams.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { UserInputError } from 'apollo-server'
|
||||||
|
|
||||||
|
export default async function replaceParams(args, context) {
|
||||||
|
const { author = 'all' } = args.filterBubble || {}
|
||||||
|
const { user } = context
|
||||||
|
|
||||||
|
if (author === 'following') {
|
||||||
|
if (!user)
|
||||||
|
throw new UserInputError(
|
||||||
|
"You are unauthenticated - I don't know any users you are following.",
|
||||||
|
)
|
||||||
|
|
||||||
|
const session = context.driver.session()
|
||||||
|
let { records } = await session.run(
|
||||||
|
'MATCH(followed:User)<-[:FOLLOWS]-(u {id: $userId}) RETURN followed.id',
|
||||||
|
{ userId: context.user.id },
|
||||||
|
)
|
||||||
|
const followedIds = records.map(record => record.get('followed.id'))
|
||||||
|
|
||||||
|
// carefully override `id_in`
|
||||||
|
args.filter = args.filter || {}
|
||||||
|
args.filter.author = args.filter.author || {}
|
||||||
|
args.filter.author.id_in = followedIds
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete args.filterBubble
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
129
backend/src/middleware/filterBubble/replaceParams.spec.js
Normal file
129
backend/src/middleware/filterBubble/replaceParams.spec.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import replaceParams from './replaceParams.js'
|
||||||
|
|
||||||
|
describe('replaceParams', () => {
|
||||||
|
let args
|
||||||
|
let context
|
||||||
|
let run
|
||||||
|
|
||||||
|
let action = () => {
|
||||||
|
return replaceParams(args, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
args = {}
|
||||||
|
run = jest.fn().mockResolvedValue({
|
||||||
|
records: [{ get: () => 1 }, { get: () => 2 }, { get: () => 3 }],
|
||||||
|
})
|
||||||
|
context = {
|
||||||
|
driver: {
|
||||||
|
session: () => {
|
||||||
|
return {
|
||||||
|
run,
|
||||||
|
close: () => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('args == ', () => {
|
||||||
|
describe('{}', () => {
|
||||||
|
it('does not crash', async () => {
|
||||||
|
await expect(action()).resolves.toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unauthenticated user', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
context.user = null
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('{ filterBubble: { author: following } }', () => {
|
||||||
|
it('throws error', async () => {
|
||||||
|
args = { filterBubble: { author: 'following' } }
|
||||||
|
await expect(action()).rejects.toThrow('You are unauthenticated')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('{ filterBubble: { author: all } }', () => {
|
||||||
|
it('removes filterBubble param', async () => {
|
||||||
|
const expected = {}
|
||||||
|
await expect(action()).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not make database calls', async () => {
|
||||||
|
await action()
|
||||||
|
expect(run).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated user', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
context.user = { id: 'u4711' }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('{ filterBubble: { author: following } }', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
args = { filterBubble: { author: 'following' } }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns args object with resolved ids of followed users', async () => {
|
||||||
|
const expected = { filter: { author: { id_in: [1, 2, 3] } } }
|
||||||
|
await expect(action()).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('makes database calls', async () => {
|
||||||
|
await action()
|
||||||
|
expect(run).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given any additional filter args', () => {
|
||||||
|
describe('merges', () => {
|
||||||
|
it('empty filter object', async () => {
|
||||||
|
args.filter = {}
|
||||||
|
const expected = { filter: { author: { id_in: [1, 2, 3] } } }
|
||||||
|
await expect(action()).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filter.title', async () => {
|
||||||
|
args.filter = { title: 'bla' }
|
||||||
|
const expected = { filter: { title: 'bla', author: { id_in: [1, 2, 3] } } }
|
||||||
|
await expect(action()).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filter.author', async () => {
|
||||||
|
args.filter = { author: { name: 'bla' } }
|
||||||
|
const expected = { filter: { author: { name: 'bla', id_in: [1, 2, 3] } } }
|
||||||
|
await expect(action()).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('{ filterBubble: { } }', () => {
|
||||||
|
it('removes filterBubble param', async () => {
|
||||||
|
const expected = {}
|
||||||
|
await expect(action()).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not make database calls', async () => {
|
||||||
|
await action()
|
||||||
|
expect(run).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('{ filterBubble: { author: all } }', () => {
|
||||||
|
it('removes filterBubble param', async () => {
|
||||||
|
const expected = {}
|
||||||
|
await expect(action()).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not make database calls', async () => {
|
||||||
|
await action()
|
||||||
|
expect(run).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -13,6 +13,7 @@ import includedFields from './includedFieldsMiddleware'
|
|||||||
import orderBy from './orderByMiddleware'
|
import orderBy from './orderByMiddleware'
|
||||||
import validation from './validation'
|
import validation from './validation'
|
||||||
import notifications from './notifications'
|
import notifications from './notifications'
|
||||||
|
import filterBubble from './filterBubble/filterBubble'
|
||||||
|
|
||||||
export default schema => {
|
export default schema => {
|
||||||
const middlewares = {
|
const middlewares = {
|
||||||
@ -30,11 +31,13 @@ export default schema => {
|
|||||||
user: user,
|
user: user,
|
||||||
includedFields: includedFields,
|
includedFields: includedFields,
|
||||||
orderBy: orderBy,
|
orderBy: orderBy,
|
||||||
|
filterBubble: filterBubble,
|
||||||
}
|
}
|
||||||
|
|
||||||
let order = [
|
let order = [
|
||||||
'permissions',
|
'permissions',
|
||||||
'activityPub',
|
'activityPub',
|
||||||
|
'filterBubble',
|
||||||
'password',
|
'password',
|
||||||
'dateTime',
|
'dateTime',
|
||||||
'validation',
|
'validation',
|
||||||
|
|||||||
@ -1,3 +1,40 @@
|
|||||||
|
enum FilterBubbleAuthorEnum {
|
||||||
|
following
|
||||||
|
all
|
||||||
|
}
|
||||||
|
|
||||||
|
input FilterBubble {
|
||||||
|
author: FilterBubbleAuthorEnum
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
Post(
|
||||||
|
id: ID
|
||||||
|
activityId: String
|
||||||
|
objectId: String
|
||||||
|
title: String
|
||||||
|
slug: String
|
||||||
|
content: String
|
||||||
|
contentExcerpt: String
|
||||||
|
image: String
|
||||||
|
imageUpload: Upload
|
||||||
|
visibility: Visibility
|
||||||
|
deleted: Boolean
|
||||||
|
disabled: Boolean
|
||||||
|
createdAt: String
|
||||||
|
updatedAt: String
|
||||||
|
commentsCount: Int
|
||||||
|
shoutedCount: Int
|
||||||
|
shoutedByCurrentUser: Boolean
|
||||||
|
_id: String
|
||||||
|
first: Int
|
||||||
|
offset: Int
|
||||||
|
orderBy: [_PostOrdering]
|
||||||
|
filter: _PostFilter
|
||||||
|
filterBubble: FilterBubble
|
||||||
|
): [Post]
|
||||||
|
}
|
||||||
|
|
||||||
type Post {
|
type Post {
|
||||||
id: ID!
|
id: ID!
|
||||||
activityId: String
|
activityId: String
|
||||||
@ -40,4 +77,4 @@ type Post {
|
|||||||
RETURN COUNT(u) >= 1
|
RETURN COUNT(u) >= 1
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user