From 927da4ffb8fb1b97534227488485d74f1ec0faca Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 30 Mar 2023 10:28:47 +0200 Subject: [PATCH 1/3] feat(backend): event parameters --- .../src/schema/resolvers/helpers/events.js | 22 +++++++++++++++++++ backend/src/schema/resolvers/posts.js | 14 ++++++++++++ backend/src/schema/types/type/Post.gql | 14 ++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 backend/src/schema/resolvers/helpers/events.js diff --git a/backend/src/schema/resolvers/helpers/events.js b/backend/src/schema/resolvers/helpers/events.js new file mode 100644 index 000000000..b725d1e5f --- /dev/null +++ b/backend/src/schema/resolvers/helpers/events.js @@ -0,0 +1,22 @@ +import { UserInputError } from 'apollo-server' + +export const validateEventParams = (params) => { + const { eventInput } = params + validateEventDate(eventInput.eventStart) + params.eventStart = eventInput.eventStart + if (eventInput.eventLocation && !eventInput.eventVenue) { + throw new UserInputError('Event venue must be present if event location is given!') + } + params.eventVenue = eventInput.eventVenue + params.eventLocation = eventInput.eventLocation +} + +const validateEventDate = (dateString) => { + const date = new Date(dateString) + if (date.toString() === 'Invalid Date') + throw new UserInputError('Event start date must be a valid date!') + const now = new Date() + if (date.getTime() < now.getTime()) { + throw new UserInputError('Event start date must be in the future!') + } +} diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index b21373d89..4afa10c77 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -7,6 +7,8 @@ import Resolver from './helpers/Resolver' import { filterForMutedUsers } from './helpers/filterForMutedUsers' import { filterInvisiblePosts } from './helpers/filterInvisiblePosts' import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups' +import { validateEventParams } from './helpers/events' +import { createOrUpdateLocations } from './users/location' import CONFIG from '../../config' const maintainPinnedPosts = (params) => { @@ -81,6 +83,15 @@ export default { CreatePost: async (_parent, params, context, _resolveInfo) => { const { categoryIds, groupId } = params const { image: imageInput } = params + + if (params.postType && params.postType === 'Event') { + validateEventParams(params) + } + delete params.eventInput + + const locationName = params.eventLocation ? params.eventLocation : null + delete params.eventLoaction + delete params.categoryIds delete params.image delete params.groupId @@ -143,6 +154,9 @@ export default { }) try { const post = await writeTxResultPromise + if (locationName) { + await createOrUpdateLocations('Post', post.id, locationName, session) + } return post } catch (e) { if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 2b4427326..c35ee7054 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -171,14 +171,26 @@ type Post { @cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)") group: Group @relation(name: "IN", direction: "OUT") + postType: [PostType] @cypher(statement: "RETURN filter(l IN labels(this) WHERE NOT l = 'Post')") + + eventLocationName: String + eventLocation: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") + eventVenue: String + eventStart: String } input _PostInput { id: ID! } +input _EventInput { + eventStart: String! + eventLocation: String + eventVenue: String +} + type Mutation { CreatePost( id: ID @@ -192,6 +204,7 @@ type Mutation { contentExcerpt: String groupId: ID postType: PostType = Article + eventInput: _EventInput ): Post UpdatePost( id: ID! @@ -204,6 +217,7 @@ type Mutation { language: String categoryIds: [ID] postType: PostType + eventInput: _EventInput ): Post DeletePost(id: ID!): Post AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED From 0c225715fa58fb58e27f68d2b60aee849ae8960e Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 30 Mar 2023 14:43:25 +0200 Subject: [PATCH 2/3] test create post with event properties --- backend/src/graphql/posts.js | 9 ++ backend/src/schema/resolvers/posts.js | 24 ++- backend/src/schema/resolvers/posts.spec.js | 169 +++++++++++++++++++-- 3 files changed, 185 insertions(+), 17 deletions(-) diff --git a/backend/src/graphql/posts.js b/backend/src/graphql/posts.js index ee01243ea..f1b62a286 100644 --- a/backend/src/graphql/posts.js +++ b/backend/src/graphql/posts.js @@ -12,6 +12,7 @@ export const createPostMutation = () => { $categoryIds: [ID] $groupId: ID $postType: PostType + $eventInput: _EventInput ) { CreatePost( id: $id @@ -21,6 +22,7 @@ export const createPostMutation = () => { categoryIds: $categoryIds groupId: $groupId postType: $postType + eventInput: $eventInput ) { id slug @@ -35,6 +37,13 @@ export const createPostMutation = () => { categories { id } + eventStart + eventLocationName + eventVenue + eventLocation { + lng + lat + } } } ` diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 4afa10c77..354ae49fa 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -89,8 +89,15 @@ export default { } delete params.eventInput - const locationName = params.eventLocation ? params.eventLocation : null - delete params.eventLoaction + let locationName + if (params.eventLocation) { + params.eventLocationName = params.eventLocation + locationName = params.eventLocation + } else { + params.eventLocationName = null + locationName = null + } + delete params.eventLocation delete params.categoryIds delete params.image @@ -406,7 +413,17 @@ export default { }, Post: { ...Resolver('Post', { - undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'], + undefinedToNull: [ + 'activityId', + 'objectId', + 'language', + 'pinnedAt', + 'pinned', + 'eventVenue', + 'eventLocation', + 'eventLocationName', + 'eventStart', + ], hasMany: { tags: '-[:TAGGED]->(related:Tag)', categories: '-[:CATEGORIZED]->(related:Category)', @@ -419,6 +436,7 @@ export default { pinnedBy: '<-[:PINNED]-(related:User)', image: '-[:HERO_IMAGE]->(related:Image)', group: '-[:IN]->(related:Group)', + eventLocation: '-[:IS_IN]->(related:Location)', }, count: { commentsCount: diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 1f9b646e8..499bd463d 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -312,19 +312,6 @@ describe('CreatePost', () => { }) }) - describe('with post type "Event"', () => { - it('has label "Event" set', async () => { - await expect( - mutate({ - mutation: createPostMutation(), - variables: { ...variables, postType: 'Event' }, - }), - ).resolves.toMatchObject({ - data: { CreatePost: { postType: ['Event'] } }, - }) - }) - }) - describe('with invalid post type', () => { it('throws an error', async () => { await expect( @@ -342,6 +329,160 @@ describe('CreatePost', () => { }) }) }) + + describe('with post type "Event"', () => { + describe('without event start date', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: createPostMutation(), + variables: { + ...variables, + postType: 'Event', + }, + }), + ).resolves.toMatchObject({ + errors: [ + { + message: "Cannot read properties of undefined (reading 'eventStart')", + }, + ], + }) + }) + }) + + describe('with invalid event start date', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: createPostMutation(), + variables: { + ...variables, + postType: 'Event', + eventInput: { + eventStart: 'no date', + }, + }, + }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Event start date must be a valid date!', + }, + ], + }) + }) + }) + + describe('with event start date in the past', () => { + it('throws an error', async () => { + const now = new Date() + await expect( + mutate({ + mutation: createPostMutation(), + variables: { + ...variables, + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth() - 1).toISOString(), + }, + }, + }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Event start date must be in the future!', + }, + ], + }) + }) + }) + + describe('event location is given but event venue is missing', () => { + it('throws an error', async () => { + const now = new Date() + await expect( + mutate({ + mutation: createPostMutation(), + variables: { + ...variables, + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + eventLocation: 'Berlin', + }, + }, + }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Event venue must be present if event location is given!', + }, + ], + }) + }) + }) + + describe('valid event input without location', () => { + it('has label "Event" set', async () => { + const now = new Date() + await expect( + mutate({ + mutation: createPostMutation(), + variables: { + ...variables, + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + }, + }, + }), + ).resolves.toMatchObject({ + data: { + CreatePost: { + postType: ['Event'], + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + }, + }, + errors: undefined, + }) + }) + }) + + describe('valid event input with location', () => { + it('has label "Event" set', async () => { + const now = new Date() + await expect( + mutate({ + mutation: createPostMutation(), + variables: { + ...variables, + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + eventLocation: 'Leipzig', + eventVenue: 'Connewitzer Kreuz', + }, + }, + }), + ).resolves.toMatchObject({ + data: { + CreatePost: { + postType: ['Event'], + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + eventLocationName: 'Leipzig', + eventVenue: 'Connewitzer Kreuz', + eventLocation: { + lng: 12.374733, + lat: 51.340632, + }, + }, + }, + errors: undefined, + }) + }) + }) + }) }) }) @@ -499,7 +640,7 @@ describe('UpdatePost', () => { }) describe('post type', () => { - it.only('changes the post type', async () => { + it('changes the post type', async () => { await expect( mutate({ mutation: updatePostMutation, variables: { ...variables, postType: 'Event' } }), ).resolves.toMatchObject({ From 2a8a993ff81591e13eb61cf5e627640d3ea80c95 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 30 Mar 2023 15:03:21 +0200 Subject: [PATCH 3/3] updatePost with event type --- .../src/schema/resolvers/helpers/events.js | 27 ++- backend/src/schema/resolvers/posts.js | 21 +-- backend/src/schema/resolvers/posts.spec.js | 166 ++++++++++++++++-- 3 files changed, 182 insertions(+), 32 deletions(-) diff --git a/backend/src/schema/resolvers/helpers/events.js b/backend/src/schema/resolvers/helpers/events.js index b725d1e5f..b2f204f1d 100644 --- a/backend/src/schema/resolvers/helpers/events.js +++ b/backend/src/schema/resolvers/helpers/events.js @@ -1,14 +1,27 @@ import { UserInputError } from 'apollo-server' export const validateEventParams = (params) => { - const { eventInput } = params - validateEventDate(eventInput.eventStart) - params.eventStart = eventInput.eventStart - if (eventInput.eventLocation && !eventInput.eventVenue) { - throw new UserInputError('Event venue must be present if event location is given!') + if (params.postType && params.postType === 'Event') { + const { eventInput } = params + validateEventDate(eventInput.eventStart) + params.eventStart = eventInput.eventStart + if (eventInput.eventLocation && !eventInput.eventVenue) { + throw new UserInputError('Event venue must be present if event location is given!') + } + params.eventVenue = eventInput.eventVenue + params.eventLocation = eventInput.eventLocation } - params.eventVenue = eventInput.eventVenue - params.eventLocation = eventInput.eventLocation + delete params.eventInput + let locationName + if (params.eventLocation) { + params.eventLocationName = params.eventLocation + locationName = params.eventLocation + } else { + params.eventLocationName = null + locationName = null + } + delete params.eventLocation + return locationName } const validateEventDate = (dateString) => { diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 354ae49fa..42a755e8f 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -84,20 +84,7 @@ export default { const { categoryIds, groupId } = params const { image: imageInput } = params - if (params.postType && params.postType === 'Event') { - validateEventParams(params) - } - delete params.eventInput - - let locationName - if (params.eventLocation) { - params.eventLocationName = params.eventLocation - locationName = params.eventLocation - } else { - params.eventLocationName = null - locationName = null - } - delete params.eventLocation + const locationName = validateEventParams(params) delete params.categoryIds delete params.image @@ -176,6 +163,9 @@ export default { UpdatePost: async (_parent, params, context, _resolveInfo) => { const { categoryIds } = params const { image: imageInput } = params + + const locationName = validateEventParams(params) + delete params.categoryIds delete params.image const session = context.driver.session() @@ -227,6 +217,9 @@ export default { return post }) const post = await writeTxResultPromise + if (locationName) { + await createOrUpdateLocations('Post', post.id, locationName, session) + } return post } finally { session.close() diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 499bd463d..b3dd28f38 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -496,6 +496,7 @@ describe('UpdatePost', () => { $image: ImageInput $categoryIds: [ID] $postType: PostType + $eventInput: _EventInput ) { UpdatePost( id: $id @@ -504,6 +505,7 @@ describe('UpdatePost', () => { image: $image categoryIds: $categoryIds postType: $postType + eventInput: $eventInput ) { id title @@ -518,6 +520,13 @@ describe('UpdatePost', () => { id } postType + eventStart + eventLocationName + eventVenue + eventLocation { + lng + lat + } } } ` @@ -639,18 +648,153 @@ describe('UpdatePost', () => { }) }) - describe('post type', () => { - it('changes the post type', async () => { - await expect( - mutate({ mutation: updatePostMutation, variables: { ...variables, postType: 'Event' } }), - ).resolves.toMatchObject({ - data: { - UpdatePost: { - id: newlyCreatedPost.id, - postType: ['Event'], + describe('change post type to event', () => { + describe('with missing event start date', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: updatePostMutation, + variables: { ...variables, postType: 'Event' }, + }), + ).resolves.toMatchObject({ + errors: [ + { + message: "Cannot read properties of undefined (reading 'eventStart')", + }, + ], + }) + }) + }) + + describe('with invalid event start date', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: updatePostMutation, + variables: { + ...variables, + postType: 'Event', + eventInput: { + eventStart: 'no-date', + }, + }, + }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Event start date must be a valid date!', + }, + ], + }) + }) + }) + + describe('with event start date in the past', () => { + it('throws an error', async () => { + const now = new Date() + await expect( + mutate({ + mutation: updatePostMutation, + variables: { + ...variables, + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth() - 1).toISOString(), + }, + }, + }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Event start date must be in the future!', + }, + ], + }) + }) + }) + + describe('event location is given but event venue is missing', () => { + it('throws an error', async () => { + const now = new Date() + await expect( + mutate({ + mutation: updatePostMutation, + variables: { + ...variables, + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + eventLocation: 'Berlin', + }, + }, + }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Event venue must be present if event location is given!', + }, + ], + }) + }) + }) + + describe('valid event input without location', () => { + it('has label "Event" set', async () => { + const now = new Date() + await expect( + mutate({ + mutation: updatePostMutation, + variables: { + ...variables, + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + }, + }, + }), + ).resolves.toMatchObject({ + data: { + UpdatePost: { + postType: ['Event'], + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + }, }, - }, - errors: undefined, + errors: undefined, + }) + }) + }) + + describe('valid event input with location', () => { + it('has label "Event" set', async () => { + const now = new Date() + await expect( + mutate({ + mutation: updatePostMutation, + variables: { + ...variables, + postType: 'Event', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + eventLocation: 'Leipzig', + eventVenue: 'Connewitzer Kreuz', + }, + }, + }), + ).resolves.toMatchObject({ + data: { + UpdatePost: { + postType: ['Event'], + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + eventLocationName: 'Leipzig', + eventVenue: 'Connewitzer Kreuz', + eventLocation: { + lng: 12.374733, + lat: 51.340632, + }, + }, + }, + errors: undefined, + }) }) }) })