From bb8a7455669d7ce653a394372590f6f2e51fd4bd Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 14 Jun 2023 16:14:19 +0200 Subject: [PATCH 1/8] simple message create mutation and query --- backend/src/schema/resolvers/message.ts | 50 +++++++++++++++++++++++ backend/src/schema/types/type/Message.gql | 25 ++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 backend/src/schema/resolvers/message.ts create mode 100644 backend/src/schema/types/type/Message.gql diff --git a/backend/src/schema/resolvers/message.ts b/backend/src/schema/resolvers/message.ts new file mode 100644 index 000000000..3558343d9 --- /dev/null +++ b/backend/src/schema/resolvers/message.ts @@ -0,0 +1,50 @@ +import { v4 as uuid } from 'uuid' +import { neo4jgraphql } from 'neo4j-graphql-js' + +export default { + Query: { + Message: async (object, params, context, resolveInfo) => { + if (!params.filter) params.filter = {} + params.filter.room = { + users_some: { + id: context.user.id, + }, + } + return neo4jgraphql(object, params, context, resolveInfo) + }, + }, + Mutation: { + CreateMessage: async (_parent, params, context, _resolveInfo) => { + const { roomId, content } = params + const { user: { id: currentUserId } } = context + const messageId = uuid() + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const createMessageCypher = ` + MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId }) + MERGE (currentUser)-[:CREATED]->(message:Message)-[:INSIDE]->(room) + SET message.createdAt = toString(datetime()), + message.id = $messageId + message.content = $content + RETURN message { .* } + ` + const createMessageTxResponse = await transaction.run( + createMessageCypher, + { currentUserId, roomId, messageId, content } + ) + const [message] = await createMessageTxResponse.records.map((record) => + record.get('message'), + ) + return message + }) + try { + const message = await writeTxResultPromise + return message + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + }, +} diff --git a/backend/src/schema/types/type/Message.gql b/backend/src/schema/types/type/Message.gql new file mode 100644 index 000000000..919e6a295 --- /dev/null +++ b/backend/src/schema/types/type/Message.gql @@ -0,0 +1,25 @@ +input _MessageFilter { + room: _RoomFilter +} + +type Message { + id: ID! + createdAt: String + updatedAt: String + + content: String! + + author: User! @relation(name: "CREATED", direction: "IN") + room: Room! @relation(name: "INSIDE", direction: "OUT") +} + +type Mutation { + CreateMessage( + roomId: ID! + content: String! + ): Message +} + +type Query { + Message(roomId: ID!): [Message] +} From ae131cc656faa0b62c483ddacadce7aa2e5fb67f Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 14 Jun 2023 16:15:28 +0200 Subject: [PATCH 2/8] add permissions for messages --- backend/src/middleware/permissionsMiddleware.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index fbca8846d..81ba93e3c 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -407,6 +407,7 @@ export default shield( availableRoles: isAdmin, getInviteCode: isAuthenticated, // and inviteRegistration Room: isAuthenticated, + Message: isAuthenticated, }, Mutation: { '*': deny, @@ -461,6 +462,7 @@ export default shield( markTeaserAsViewed: allow, saveCategorySettings: isAuthenticated, CreateRoom: isAuthenticated, + CreateMessage: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), From d2beca22c939c488c504b5f2fc00a6a28f30efd2 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 14 Jun 2023 17:27:42 +0200 Subject: [PATCH 3/8] set up unit tests for messages --- backend/src/graphql/messages.ts | 32 +++++++ backend/src/schema/resolvers/messages.spec.ts | 93 +++++++++++++++++++ .../resolvers/{message.ts => messages.ts} | 2 +- 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 backend/src/graphql/messages.ts create mode 100644 backend/src/schema/resolvers/messages.spec.ts rename backend/src/schema/resolvers/{message.ts => messages.ts} (98%) diff --git a/backend/src/graphql/messages.ts b/backend/src/graphql/messages.ts new file mode 100644 index 000000000..59694914a --- /dev/null +++ b/backend/src/graphql/messages.ts @@ -0,0 +1,32 @@ +import gql from 'graphql-tag' + +export const createMessageMutation = () => { + return gql` + mutation ( + $roomId: ID! + $content: String! + ) { + CreateMessage( + roomId: $roomId + content: $content + ) { + id + content + } + } + ` +} + +export const messageQuery = () => { + return gql` + query($roomId: ID!) { + Message(roomId: $roomId) { + id + content + author { + id + } + } + } + ` +} diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts new file mode 100644 index 000000000..b1fc0b208 --- /dev/null +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -0,0 +1,93 @@ +import { createTestClient } from 'apollo-server-testing' +import Factory, { cleanDatabase } from '../../db/factories' +import { getNeode, getDriver } from '../../db/neo4j' +import { createRoomMutation } from '../../graphql/rooms' +import { createMessageMutation, messageQuery } from '../../graphql/messages' +import createServer from '../../server' + +const driver = getDriver() +const neode = getNeode() + +let query +let mutate +let authenticatedUser +let chattingUser, otherChattingUser, notChattingUser + +beforeAll(async () => { + await cleanDatabase() + + const { server } = createServer({ + context: () => { + return { + driver, + neode, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query + mutate = createTestClient(server).mutate +}) + +afterAll(async () => { + // await cleanDatabase() + driver.close() +}) + + +describe('Message', () => { + beforeAll(async () => { + ;[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([ + Factory.build( + 'user', + { + id: 'chatting-user', + name: 'Chatting User', + }, + ), + Factory.build( + 'user', + { + id: 'other-chatting-user', + name: 'Other Chatting User', + }, + ), + Factory.build( + 'user', + { + id: 'not-chatting-user', + name: 'Not Chatting User', + }, + ), + ]) + }) + + describe('create message', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + await expect(mutate({ mutation: createMessageMutation(), variables: { + roomId: 'some-id', content: 'Some bla bla bla', } })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + authenticatedUser = await chattingUser.toJson() + }) + + describe('room does not exist', () => { + it('returns null', async () => { + await expect(mutate({ mutation: createMessageMutation(), variables: { + roomId: 'some-id', content: 'Some bla bla bla', } })).resolves.toMatchObject({ + errors: undefined, + data: { + CreateMessage: null, + }, + }) + }) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/message.ts b/backend/src/schema/resolvers/messages.ts similarity index 98% rename from backend/src/schema/resolvers/message.ts rename to backend/src/schema/resolvers/messages.ts index 3558343d9..44610fb83 100644 --- a/backend/src/schema/resolvers/message.ts +++ b/backend/src/schema/resolvers/messages.ts @@ -24,7 +24,7 @@ export default { MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId }) MERGE (currentUser)-[:CREATED]->(message:Message)-[:INSIDE]->(room) SET message.createdAt = toString(datetime()), - message.id = $messageId + message.id = $messageId, message.content = $content RETURN message { .* } ` From 610d538ca999a1576ae37ad4347f2f88f8457b3c Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 15 Jun 2023 16:26:55 +0200 Subject: [PATCH 4/8] message query is not working as expected --- backend/src/schema/resolvers/messages.spec.ts | 116 ++++++++++++++++++ backend/src/schema/resolvers/messages.ts | 19 ++- backend/src/schema/types/type/Room.gql | 2 + 3 files changed, 135 insertions(+), 2 deletions(-) diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index b1fc0b208..e95d5d256 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -36,6 +36,8 @@ afterAll(async () => { describe('Message', () => { + let roomId: string + beforeAll(async () => { ;[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([ Factory.build( @@ -88,6 +90,120 @@ describe('Message', () => { }) }) }) + + describe('room exists', () => { + beforeAll(async () => { + const room = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'other-chatting-user', + }, + }) + roomId = room.data.CreateRoom.id + }) + + describe('user chats in room', () => { + it('returns the message', async () => { + await expect(mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Some nice message to other chatting user', + } })).resolves.toMatchObject({ + errors: undefined, + data: { + CreateMessage: { + id: expect.any(String), + content: 'Some nice message to other chatting user', + }, + }, + }) + }) + }) + + describe('user does not chat in room', () => { + beforeAll(async () => { + authenticatedUser = await notChattingUser.toJson() + }) + + it('returns null', async () => { + await expect(mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'I have no access to this room!', + } })).resolves.toMatchObject({ + errors: undefined, + data: { + CreateMessage: null, + }, + }) + }) + }) + }) + }) + }) + + describe('message query', () => { + describe('unauthenticated', () => { + beforeAll(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect(query({ + query: messageQuery(), + variables: { + roomId: 'some-id' } + })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + authenticatedUser = await otherChattingUser.toJson() + }) + + describe('room does not exists', () => { + it('returns null', async () => { + await expect(query({ + query: messageQuery(), + variables: { + roomId: 'some-id' + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + Message: [], + }, + }) + }) + }) + + describe('room exists with authenticated user chatting', () => { + it('returns null', async () => { + console.log(roomId) + await expect(query({ + query: messageQuery(), + variables: { + roomId, + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + Message: [{ + id: expect.any(String), + content: 'Some nice message to other chatting user', + author: { + id: 'chatting-user', + }, + }], + }, + }) + }) + }) }) }) }) diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/schema/resolvers/messages.ts index 44610fb83..ac090e78f 100644 --- a/backend/src/schema/resolvers/messages.ts +++ b/backend/src/schema/resolvers/messages.ts @@ -1,15 +1,22 @@ import { v4 as uuid } from 'uuid' import { neo4jgraphql } from 'neo4j-graphql-js' +import Resolver from './helpers/Resolver' export default { Query: { - Message: async (object, params, context, resolveInfo) => { - if (!params.filter) params.filter = {} + Message: async (object, params, context, resolveInfo) => { + console.log('message query', params) + const { roomId } = params + // if (!params.filter) params.filter = {} + /* params.filter.room = { + id_in: [roomId], users_some: { id: context.user.id, }, } + */ + console.log(params.filter) return neo4jgraphql(object, params, context, resolveInfo) }, }, @@ -47,4 +54,12 @@ export default { } }, }, + Message: { + ...Resolver('Message', { + hasOne: { + author: '<-[:CREATED]-(related:User)', + room: '-[:INSIDE]->(related:Room)', + } + }), + } } diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql index b3b5ea913..0cff02a32 100644 --- a/backend/src/schema/types/type/Room.gql +++ b/backend/src/schema/types/type/Room.gql @@ -1,6 +1,8 @@ input _RoomFilter { AND: [_RoomFilter!] OR: [_RoomFilter!] + id: ID + id_in: [ID!] users_some: _UserFilter } From 90bf1881f7b58766d3c3822e26b62b4a6fca538b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 15 Jun 2023 16:45:38 +0200 Subject: [PATCH 5/8] message query is working --- backend/src/schema/resolvers/messages.spec.ts | 25 ++++++++++++++++--- backend/src/schema/resolvers/messages.ts | 9 +++---- backend/src/schema/types/type/Room.gql | 1 - 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index e95d5d256..803418aad 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -183,8 +183,7 @@ describe('Message', () => { }) describe('room exists with authenticated user chatting', () => { - it('returns null', async () => { - console.log(roomId) + it('returns the messages', async () => { await expect(query({ query: messageQuery(), variables: { @@ -203,7 +202,27 @@ describe('Message', () => { }, }) }) - }) + }) + + describe('room exists, authenticated user not in room', () => { + beforeAll(async () => { + authenticatedUser = await notChattingUser.toJson() + }) + + it('returns null', async () => { + await expect(query({ + query: messageQuery(), + variables: { + roomId, + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + Message: [], + }, + }) + }) + }) }) }) }) diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/schema/resolvers/messages.ts index ac090e78f..b03b5e648 100644 --- a/backend/src/schema/resolvers/messages.ts +++ b/backend/src/schema/resolvers/messages.ts @@ -5,18 +5,15 @@ import Resolver from './helpers/Resolver' export default { Query: { Message: async (object, params, context, resolveInfo) => { - console.log('message query', params) const { roomId } = params - // if (!params.filter) params.filter = {} - /* + delete params.roomId + if (!params.filter) params.filter = {} params.filter.room = { - id_in: [roomId], + id: roomId, users_some: { id: context.user.id, }, } - */ - console.log(params.filter) return neo4jgraphql(object, params, context, resolveInfo) }, }, diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql index 0cff02a32..c228c3a74 100644 --- a/backend/src/schema/types/type/Room.gql +++ b/backend/src/schema/types/type/Room.gql @@ -2,7 +2,6 @@ input _RoomFilter { AND: [_RoomFilter!] OR: [_RoomFilter!] id: ID - id_in: [ID!] users_some: _UserFilter } From 8f86d6fc824b1cebe26462318104c38f852157a3 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Jun 2023 10:54:41 +0200 Subject: [PATCH 6/8] remove message filter from schema, use apoc create uuid --- backend/src/schema/resolvers/messages.spec.ts | 2 +- backend/src/schema/resolvers/messages.ts | 11 +++++------ backend/src/schema/resolvers/rooms.spec.ts | 2 +- backend/src/schema/types/type/Message.gql | 6 +++--- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index 803418aad..163788692 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -30,7 +30,7 @@ beforeAll(async () => { }) afterAll(async () => { - // await cleanDatabase() + await cleanDatabase() driver.close() }) diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/schema/resolvers/messages.ts index b03b5e648..2cf72e9fe 100644 --- a/backend/src/schema/resolvers/messages.ts +++ b/backend/src/schema/resolvers/messages.ts @@ -1,4 +1,3 @@ -import { v4 as uuid } from 'uuid' import { neo4jgraphql } from 'neo4j-graphql-js' import Resolver from './helpers/Resolver' @@ -21,20 +20,20 @@ export default { CreateMessage: async (_parent, params, context, _resolveInfo) => { const { roomId, content } = params const { user: { id: currentUserId } } = context - const messageId = uuid() const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { const createMessageCypher = ` MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId }) MERGE (currentUser)-[:CREATED]->(message:Message)-[:INSIDE]->(room) - SET message.createdAt = toString(datetime()), - message.id = $messageId, - message.content = $content + ON CREATE SET + message.createdAt = toString(datetime()), + message.id = apoc.create.uuid(), + message.content = $content RETURN message { .* } ` const createMessageTxResponse = await transaction.run( createMessageCypher, - { currentUserId, roomId, messageId, content } + { currentUserId, roomId, content } ) const [message] = await createMessageTxResponse.records.map((record) => record.get('message'), diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 1c9333bd7..8c4d887cb 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -29,7 +29,7 @@ beforeAll(async () => { }) afterAll(async () => { - // await cleanDatabase() + await cleanDatabase() driver.close() }) diff --git a/backend/src/schema/types/type/Message.gql b/backend/src/schema/types/type/Message.gql index 919e6a295..5b14104db 100644 --- a/backend/src/schema/types/type/Message.gql +++ b/backend/src/schema/types/type/Message.gql @@ -1,6 +1,6 @@ -input _MessageFilter { - room: _RoomFilter -} +# input _MessageFilter { +# room: _RoomFilter +# } type Message { id: ID! From 1ee8e488a37e4fd4c2fd54a0b54404931c24ceef Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Jun 2023 11:04:43 +0200 Subject: [PATCH 7/8] Update backend/src/schema/resolvers/messages.spec.ts Co-authored-by: Ulf Gebhardt --- backend/src/schema/resolvers/messages.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index 163788692..ecdcfd9a6 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -39,7 +39,7 @@ describe('Message', () => { let roomId: string beforeAll(async () => { - ;[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([ + [chattingUser, otherChattingUser, notChattingUser] = await Promise.all([ Factory.build( 'user', { From 45ef5be81b781eadc2f406fc521c155a6ce8987a Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Jun 2023 11:20:44 +0200 Subject: [PATCH 8/8] test with second message --- backend/src/schema/resolvers/messages.spec.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index ecdcfd9a6..e9cf26a22 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -202,6 +202,47 @@ describe('Message', () => { }, }) }) + + describe('more messages', () => { + beforeAll(async () => { + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Another nice message to other chatting user', + } + }) + }) + + it('returns the messages', async () => { + await expect(query({ + query: messageQuery(), + variables: { + roomId, + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + Message: [ + { + id: expect.any(String), + content: 'Some nice message to other chatting user', + author: { + id: 'chatting-user', + }, + }, + { + id: expect.any(String), + content: 'Another nice message to other chatting user', + author: { + id: 'other-chatting-user', + }, + } + ], + }, + }) + }) + }) }) describe('room exists, authenticated user not in room', () => {