Merge pull request #2243 from Human-Connection/refactor_neo4j-graphql-js

Explicitly define our schema, improve performance
This commit is contained in:
mattwr18 2019-11-18 21:46:56 +01:00 committed by GitHub
commit 19d2e2cd41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 279 additions and 221 deletions

View File

@ -1,12 +0,0 @@
import {
GraphQLLowerCaseDirective,
GraphQLTrimDirective,
GraphQLDefaultToDirective,
} from 'graphql-custom-directives'
export default function applyDirectives(augmentedSchema) {
const directives = [GraphQLLowerCaseDirective, GraphQLTrimDirective, GraphQLDefaultToDirective]
augmentedSchema._directives.push.apply(augmentedSchema._directives, directives)
return augmentedSchema
}

View File

@ -1,9 +0,0 @@
import { GraphQLDate, GraphQLTime, GraphQLDateTime } from 'graphql-iso-date'
export default function applyScalars(augmentedSchema) {
augmentedSchema._typeMap.Date = GraphQLDate
augmentedSchema._typeMap.Time = GraphQLTime
augmentedSchema._typeMap.DateTime = GraphQLDateTime
return augmentedSchema
}

View File

@ -41,20 +41,6 @@ const isMySocialMedia = rule({
return socialMedia.ownedBy.node.id === user.id
})
/* TODO: decide if we want to remove this check: the check
* `onlyEnabledContent` throws authorization errors only if you have
* arguments for `disabled` or `deleted` assuming these are filter
* parameters. Soft-delete middleware obfuscates data on its way out
* anyways. Furthermore, `neo4j-graphql-js` offers many ways to filter for
* data so I believe, this is not a good check anyways.
*/
const onlyEnabledContent = rule({
cache: 'strict',
})(async (parent, args, ctx, info) => {
const { disabled, deleted } = args
return !(disabled || deleted)
})
const invitationLimitReached = rule({
cache: 'no_cache',
})(async (parent, args, { user, driver }) => {
@ -125,7 +111,8 @@ const permissions = shield(
reports: isModerator,
statistics: allow,
currentUser: allow,
Post: or(onlyEnabledContent, isModerator),
Post: allow,
profilePagePosts: allow,
Comment: allow,
User: or(noEmailFilter, isAdmin),
isLoggedIn: allow,
@ -134,7 +121,6 @@ const permissions = shield(
PostsEmotionsByCurrentUser: isAuthenticated,
blockedUsers: isAuthenticated,
notifications: isAuthenticated,
profilePagePosts: or(onlyEnabledContent, isModerator),
Donations: isAuthenticated,
},
Mutation: {

View File

@ -25,9 +25,5 @@ export default {
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info)
},
CreateCategory: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Category')))
return resolve(root, args, context, info)
},
},
}

View File

@ -3,9 +3,7 @@ const isModerator = ({ user }) => {
}
const setDefaultFilters = (resolve, root, args, context, info) => {
if (typeof args.deleted !== 'boolean') {
args.deleted = false
}
args.deleted = false
if (!isModerator(context)) {
args.disabled = false

View File

@ -341,76 +341,6 @@ describe('softDeleteMiddleware', () => {
})
})
})
describe('filter (deleted: true)', () => {
beforeEach(() => {
graphqlQuery = gql`
{
Post(deleted: true) {
title
}
}
`
})
describe('as user', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
})
it('throws authorisation error', async () => {
const { data, errors } = await action()
expect(data).toEqual({ Post: null })
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('as moderator', () => {
beforeEach(async () => {
authenticatedUser = await moderator.toJson()
})
it('does not show deleted posts', async () => {
const expected = { data: { Post: [{ title: 'UNAVAILABLE' }] } }
await expect(action()).resolves.toMatchObject(expected)
})
})
})
describe('filter (disabled: true)', () => {
beforeEach(() => {
graphqlQuery = gql`
{
Post(disabled: true) {
title
}
}
`
})
describe('as user', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
})
it('throws authorisation error', async () => {
const { data, errors } = await action()
expect(data).toEqual({ Post: null })
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('as moderator', () => {
beforeEach(async () => {
authenticatedUser = await moderator.toJson()
})
it('shows disabled posts', async () => {
const expected = { data: { Post: [{ title: 'Disabled post' }] } }
await expect(action()).resolves.toMatchObject(expected)
})
})
})
})
})
})

View File

@ -1,9 +0,0 @@
export const undefinedToNull = list => {
const resolvers = {}
list.forEach(key => {
resolvers[key] = async (parent, params, context, resolveInfo) => {
return typeof parent[key] === 'undefined' ? null : parent[key]
}
})
return resolvers
}

View File

@ -1,56 +1,27 @@
import { makeAugmentedSchema } from 'neo4j-graphql-js'
import CONFIG from './../config'
import applyScalars from './../bootstrap/scalars'
import applyDirectives from './../bootstrap/directives'
import typeDefs from './types'
import resolvers from './resolvers'
export default applyScalars(
applyDirectives(
makeAugmentedSchema({
typeDefs,
resolvers,
config: {
query: {
exclude: [
'Badge',
'Embed',
'InvitationCode',
'EmailAddress',
'Notfication',
'Statistics',
'LoggedInUser',
'Location',
'SocialMedia',
'NOTIFIED',
'REPORTED',
'Donations',
],
// add 'User' here as soon as possible
},
mutation: {
exclude: [
'Badge',
'Embed',
'InvitationCode',
'EmailAddress',
'Notfication',
'Post',
'Comment',
'Statistics',
'LoggedInUser',
'Location',
'SocialMedia',
'User',
'EMOTED',
'NOTIFIED',
'REPORTED',
'Donations',
],
// add 'User' here as soon as possible
},
debug: !!CONFIG.DEBUG,
},
}),
),
)
export default makeAugmentedSchema({
typeDefs,
resolvers,
config: {
query: {
exclude: [
'Badge',
'Embed',
'InvitationCode',
'EmailAddress',
'Notfication',
'Statistics',
'LoggedInUser',
'Location',
'SocialMedia',
'NOTIFIED',
'REPORTED',
'Donations',
],
},
mutation: false,
},
})

View File

@ -29,7 +29,7 @@ const filterForBlockedUsers = async (params, context) => {
}
const maintainPinnedPosts = params => {
const pinnedPostFilter = { pinnedBy_in: { role_in: ['admin'] } }
const pinnedPostFilter = { pinned: true }
if (isEmpty(params.filter)) {
params.filter = { OR: [pinnedPostFilter, {}] }
} else {

View File

@ -210,6 +210,7 @@ describe('Post', () => {
data: {
Post: expect.arrayContaining(expected),
},
errors: undefined,
})
})
})
@ -229,7 +230,9 @@ describe('Post', () => {
await user.relateTo(followedUser, 'following')
variables = { filter: { author: { followedBy_some: { id: 'current-user' } } } }
const expected = {
await expect(
query({ query: postQueryFilteredByUsersFollowed, variables }),
).resolves.toMatchObject({
data: {
Post: [
{
@ -238,10 +241,8 @@ describe('Post', () => {
},
],
},
}
await expect(
query({ query: postQueryFilteredByUsersFollowed, variables }),
).resolves.toMatchObject(expected)
errors: undefined,
})
})
})
})

View File

@ -1 +0,0 @@
scalar Date

View File

@ -1 +0,0 @@
scalar DateTime

View File

@ -1 +0,0 @@
scalar Time

View File

@ -3,8 +3,6 @@ type Badge {
type: BadgeType!
status: BadgeStatus!
icon: String!
#createdAt: DateTime
#updatedAt: DateTime
createdAt: String
updatedAt: String

View File

@ -1,13 +1,41 @@
enum _CategoryOrdering {
id_asc
id_desc
name_asc
name_desc
slug_asc
slug_desc
icon_asc
icon_desc
createdAt_asc
createdAt_desc
updatedAt_asc
updatedAt_desc
postCount_asc
postCount_desc
}
type Category {
id: ID!
name: String!
slug: String
icon: String!
#createdAt: DateTime
#updatedAt: DateTime
createdAt: String
updatedAt: String
posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN")
postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)")
}
type Query {
Category(
id: ID
name: String
slug: String
icon: String
createdAt: String
updatedAt: String
first: Int
offset: Int
orderBy: [_CategoryOrdering]
): [Category]
}

View File

@ -1,3 +1,41 @@
enum _CommentOrdering {
id_asc
id_desc
content_asc
content_desc
createdAt_asc
createdAt_desc
updatedAt_asc
updatedAt_desc
}
input _CommentFilter {
AND: [_CommentFilter!]
OR: [_CommentFilter!]
id: ID
id_not: ID
id_in: [ID!]
id_not_in: [ID!]
author: _UserFilter
author_not: _UserFilter
author_in: [_UserFilter!]
author_not_in: [_UserFilter!]
content: String
content_not: String
content_in: [String!]
content_not_in: [String!]
content_contains: String
content_not_contains: String
content_starts_with: String
content_not_starts_with: String
content_ends_with: String
content_not_ends_with: String
post: _PostFilter
post_not: _PostFilter
post_in: [_PostFilter!]
post_not_in: [_PostFilter!]
}
type Comment {
id: ID!
activityId: String
@ -12,6 +50,19 @@ type Comment {
disabledBy: User @relation(name: "DISABLED", direction: "IN")
}
type Query {
Comment(
id: ID
content: String
createdAt: String
updatedAt: String
first: Int
offset: Int
orderBy: [_CommentOrdering]
filter: _CommentFilter
): [Comment]
}
type Mutation {
CreateComment(
id: ID

View File

@ -3,8 +3,6 @@ type EMOTED @relation(name: "EMOTED") {
to: Post
emotion: Emotion
# createdAt: DateTime
# updatedAt: DateTime
createdAt: String
updatedAt: String
}

View File

@ -2,9 +2,6 @@ type InvitationCode {
id: ID!
token: String
generatedBy: User @relation(name: "GENERATED", direction: "IN")
#createdAt: DateTime
#usedAt: DateTime
createdAt: String
}

View File

@ -1,26 +1,101 @@
input _PostFilter {
AND: [_PostFilter!]
OR: [_PostFilter!]
id: ID
id_not: ID
id_in: [ID!]
id_not_in: [ID!]
author: _UserFilter
author_not: _UserFilter
author_in: [_UserFilter!]
author_not_in: [_UserFilter!]
title: String
title_not: String
title_in: [String!]
title_not_in: [String!]
title_contains: String
title_not_contains: String
title_starts_with: String
title_not_starts_with: String
title_ends_with: String
title_not_ends_with: String
slug: String
slug_not: String
slug_in: [String!]
slug_not_in: [String!]
slug_contains: String
slug_not_contains: String
slug_starts_with: String
slug_not_starts_with: String
slug_ends_with: String
slug_not_ends_with: String
content: String
content_not: String
content_in: [String!]
content_not_in: [String!]
content_contains: String
content_not_contains: String
content_starts_with: String
content_not_starts_with: String
content_ends_with: String
content_not_ends_with: String
image: String
visibility: Visibility
visibility_not: Visibility
visibility_in: [Visibility!]
visibility_not_in: [Visibility!]
language: String
language_not: String
language_in: [String!]
language_not_in: [String!]
pinned: Boolean # required for `maintainPinnedPost`
tags: _TagFilter
tags_not: _TagFilter
tags_in: [_TagFilter!]
tags_not_in: [_TagFilter!]
tags_some: _TagFilter
tags_none: _TagFilter
tags_single: _TagFilter
tags_every: _TagFilter
categories: _CategoryFilter
categories_not: _CategoryFilter
categories_in: [_CategoryFilter!]
categories_not_in: [_CategoryFilter!]
categories_some: _CategoryFilter
categories_none: _CategoryFilter
categories_single: _CategoryFilter
categories_every: _CategoryFilter
comments: _CommentFilter
comments_not: _CommentFilter
comments_in: [_CommentFilter!]
comments_not_in: [_CommentFilter!]
comments_some: _CommentFilter
comments_none: _CommentFilter
comments_single: _CommentFilter
comments_every: _CommentFilter
emotions: _PostEMOTEDFilter
emotions_not: _PostEMOTEDFilter
emotions_in: [_PostEMOTEDFilter!]
emotions_not_in: [_PostEMOTEDFilter!]
emotions_some: _PostEMOTEDFilter
emotions_none: _PostEMOTEDFilter
emotions_single: _PostEMOTEDFilter
emotions_every: _PostEMOTEDFilter
}
enum _PostOrdering {
id_asc
id_desc
activityId_asc
activityId_desc
objectId_asc
objectId_desc
title_asc
title_desc
slug_asc
slug_desc
content_asc
content_desc
contentExcerpt_asc
contentExcerpt_desc
image_asc
image_desc
visibility_asc
visibility_desc
deleted_asc
deleted_desc
disabled_asc
disabled_desc
createdAt_asc
createdAt_desc
updatedAt_asc
@ -79,7 +154,7 @@ type Post {
@cypher(
statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)"
)
# Has the currently logged in user shouted that post?
shoutedByCurrentUser: Boolean!
@cypher(
@ -128,6 +203,22 @@ type Mutation {
}
type Query {
Post(
id: ID
title: String
slug: String
content: String
image: String
visibility: Visibility
pinned: Boolean
createdAt: String
updatedAt: String
language: String
first: Int
offset: Int
orderBy: [_PostOrdering]
filter: _PostFilter
): [Post]
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
PostsEmotionsByCurrentUser(postId: ID!): [String]
profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post]

View File

@ -1,3 +1,20 @@
input _TagFilter {
AND: [_TagFilter!]
OR: [_TagFilter!]
id: ID
id_not: ID
id_in: [ID!]
id_not_in: [ID!]
taggedPosts: _PostFilter
taggedPosts_not: _PostFilter
taggedPosts_in: [_PostFilter!]
taggedPosts_not_in: [_PostFilter!]
taggedPosts_some: _PostFilter
taggedPosts_none: _PostFilter
taggedPosts_single: _PostFilter
taggedPosts_every: _PostFilter
}
type Tag {
id: ID!
taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
@ -6,3 +23,22 @@ type Tag {
deleted: Boolean
disabled: Boolean
}
enum _TagOrdering {
id_asc
id_desc
taggedCount_asc
taggedCount_desc
taggedCountUnique_asc
taggedCountUnique_desc
}
type Query {
Tag(
id: ID
first: Int
offset: Int
orderBy: [_TagOrdering]
filter: _TagFilter
): [Tag]
}

View File

@ -1,3 +1,28 @@
enum _UserOrdering {
id_asc
id_desc
name_asc
name_desc
slug_asc
slug_desc
avatar_asc
avatar_desc
coverImg_asc
coverImg_desc
role_asc
role_desc
locationName_asc
locationName_desc
about_asc
about_desc
createdAt_asc
createdAt_desc
updatedAt_asc
updatedAt_desc
locale_asc
locale_desc
}
type User {
id: ID!
actorId: String
@ -92,12 +117,6 @@ input _UserFilter {
id_not: ID
id_in: [ID!]
id_not_in: [ID!]
id_contains: ID
id_not_contains: ID
id_starts_with: ID
id_not_starts_with: ID
id_ends_with: ID
id_not_ends_with: ID
friends: _UserFilter
friends_not: _UserFilter
friends_in: [_UserFilter!]
@ -128,8 +147,7 @@ input _UserFilter {
type Query {
User(
id: ID
email: String
actorId: String
email: String # admins need to search for a user sometimes
name: String
slug: String
avatar: String
@ -139,14 +157,6 @@ type Query {
about: String
createdAt: String
updatedAt: String
friendsCount: Int
followingCount: Int
followedByCount: Int
followedByCurrentUser: Boolean
contributionsCount: Int
commentedCount: Int
shoutedCount: Int
badgesCount: Int
first: Int
offset: Int
orderBy: [_UserOrdering]