diff --git a/backend/src/bootstrap/directives.js b/backend/src/bootstrap/directives.js deleted file mode 100644 index 93a7574fb..000000000 --- a/backend/src/bootstrap/directives.js +++ /dev/null @@ -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 -} diff --git a/backend/src/bootstrap/scalars.js b/backend/src/bootstrap/scalars.js deleted file mode 100644 index eb6d3739b..000000000 --- a/backend/src/bootstrap/scalars.js +++ /dev/null @@ -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 -} diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index d312bc112..b0d07c8ec 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -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: { diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index 03d7f8584..fed9b4da7 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -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) - }, }, } diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.js b/backend/src/middleware/softDelete/softDeleteMiddleware.js index 3360d4085..8be8c3d39 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.js +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.js @@ -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 diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js index 5b04abebd..fa942f5c4 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js @@ -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) - }) - }) - }) }) }) }) diff --git a/backend/src/schema/helpers.js b/backend/src/schema/helpers.js deleted file mode 100644 index fe61ccf57..000000000 --- a/backend/src/schema/helpers.js +++ /dev/null @@ -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 -} diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index b1bd36451..516f47abd 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -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, + }, +}) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 0f2fd8e78..ee6a82d42 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -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 { diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 9106e4eb9..005c18cd7 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -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, + }) }) }) }) diff --git a/backend/src/schema/types/scalar/Date.gql_ b/backend/src/schema/types/scalar/Date.gql_ deleted file mode 100644 index 7b0004ea3..000000000 --- a/backend/src/schema/types/scalar/Date.gql_ +++ /dev/null @@ -1 +0,0 @@ -scalar Date \ No newline at end of file diff --git a/backend/src/schema/types/scalar/DateTime.gql_ b/backend/src/schema/types/scalar/DateTime.gql_ deleted file mode 100644 index af973932f..000000000 --- a/backend/src/schema/types/scalar/DateTime.gql_ +++ /dev/null @@ -1 +0,0 @@ -scalar DateTime \ No newline at end of file diff --git a/backend/src/schema/types/scalar/Time.gql_ b/backend/src/schema/types/scalar/Time.gql_ deleted file mode 100644 index 53becdd66..000000000 --- a/backend/src/schema/types/scalar/Time.gql_ +++ /dev/null @@ -1 +0,0 @@ -scalar Time \ No newline at end of file diff --git a/backend/src/schema/types/type/Badge.gql b/backend/src/schema/types/type/Badge.gql index 99015a518..dff1de89a 100644 --- a/backend/src/schema/types/type/Badge.gql +++ b/backend/src/schema/types/type/Badge.gql @@ -3,8 +3,6 @@ type Badge { type: BadgeType! status: BadgeStatus! icon: String! - #createdAt: DateTime - #updatedAt: DateTime createdAt: String updatedAt: String diff --git a/backend/src/schema/types/type/Category.gql b/backend/src/schema/types/type/Category.gql index 9ee628d76..39efeff9d 100644 --- a/backend/src/schema/types/type/Category.gql +++ b/backend/src/schema/types/type/Category.gql @@ -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] +} diff --git a/backend/src/schema/types/type/Comment.gql b/backend/src/schema/types/type/Comment.gql index 1ccf617ef..ba9d7a3fc 100644 --- a/backend/src/schema/types/type/Comment.gql +++ b/backend/src/schema/types/type/Comment.gql @@ -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 diff --git a/backend/src/schema/types/type/EMOTED.gql b/backend/src/schema/types/type/EMOTED.gql index ee1576517..7a8b47b73 100644 --- a/backend/src/schema/types/type/EMOTED.gql +++ b/backend/src/schema/types/type/EMOTED.gql @@ -3,8 +3,6 @@ type EMOTED @relation(name: "EMOTED") { to: Post emotion: Emotion - # createdAt: DateTime - # updatedAt: DateTime createdAt: String updatedAt: String } diff --git a/backend/src/schema/types/type/InvitationCode.gql b/backend/src/schema/types/type/InvitationCode.gql index 044967286..61ce0f689 100644 --- a/backend/src/schema/types/type/InvitationCode.gql +++ b/backend/src/schema/types/type/InvitationCode.gql @@ -2,9 +2,6 @@ type InvitationCode { id: ID! token: String generatedBy: User @relation(name: "GENERATED", direction: "IN") - - #createdAt: DateTime - #usedAt: DateTime createdAt: String } diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index b4d98ec5c..a52558071 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -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] diff --git a/backend/src/schema/types/type/Tag.gql b/backend/src/schema/types/type/Tag.gql index 84c6ee7e7..41a772e4d 100644 --- a/backend/src/schema/types/type/Tag.gql +++ b/backend/src/schema/types/type/Tag.gql @@ -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] +} diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index bf37cb610..53e739988 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -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]