From d548fc2b842e549802d738b45bb249f83dbcb0a8 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 14 Jun 2023 12:09:52 +0200 Subject: [PATCH 01/11] simple create room mutation --- .../src/middleware/permissionsMiddleware.ts | 1 + backend/src/schema/resolvers/rooms.ts | 34 +++++++++++++++++++ backend/src/schema/types/type/Room.gql | 13 +++++++ 3 files changed, 48 insertions(+) create mode 100644 backend/src/schema/resolvers/rooms.ts create mode 100644 backend/src/schema/types/type/Room.gql diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index 6cd8f39d6..6498bba2f 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -459,6 +459,7 @@ export default shield( switchUserRole: isAdmin, markTeaserAsViewed: allow, saveCategorySettings: isAuthenticated, + CreateRoom: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts new file mode 100644 index 000000000..26a5e7009 --- /dev/null +++ b/backend/src/schema/resolvers/rooms.ts @@ -0,0 +1,34 @@ +export default { + Mutation: { + CreateRoom: async (_parent, params, context, _resolveInfo) => { + const { userId } = params + const { user: { id: currentUserId } } = context + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const createRoomCypher = ` + MATCH (currentUser:User { id: $currentUserId }) + MATCH (user:User { id: $userId }) + MERGE (currentUser)-[:CHATS_IN]->(room:Room)<-[:CHATS_IN]-(user) + SET room.createdAt = toString(datetime()) + RETURN room + ` + const createRommTxResponse = await await transaction.run( + createRoomCypher, + { userId, currentUserId } + ) + const [room] = await createRommTxResponse.records.map((record) => + record.get('room'), + ) + return room + }) + try { + const room = await writeTxResultPromise + return room + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + } + } +} diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql new file mode 100644 index 000000000..0127021cb --- /dev/null +++ b/backend/src/schema/types/type/Room.gql @@ -0,0 +1,13 @@ +type Room { + id: ID! + createdAt: String + updatedAt: String + + users: [User!]! @relation(name: "CHATS_IN", direction: "IN") +} + +type Mutation { + CreateRoom( + userId: ID! + ): Room +} From 8b037ffa73af013b70c4afcc92f8fcbf7bb961ce Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 14 Jun 2023 12:16:44 +0200 Subject: [PATCH 02/11] fix room properties --- backend/src/schema/resolvers/rooms.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index 26a5e7009..6d64c49b3 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -1,20 +1,24 @@ +import { v4 as uuid } from 'uuid' + export default { Mutation: { CreateRoom: async (_parent, params, context, _resolveInfo) => { const { userId } = params const { user: { id: currentUserId } } = context + const roomId = uuid() const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { const createRoomCypher = ` MATCH (currentUser:User { id: $currentUserId }) MATCH (user:User { id: $userId }) MERGE (currentUser)-[:CHATS_IN]->(room:Room)<-[:CHATS_IN]-(user) - SET room.createdAt = toString(datetime()) - RETURN room + SET room.createdAt = toString(datetime()), + room.id = $roomId + RETURN room { .* } ` const createRommTxResponse = await await transaction.run( createRoomCypher, - { userId, currentUserId } + { userId, currentUserId, roomId } ) const [room] = await createRommTxResponse.records.map((record) => record.get('room'), From fc50d69bff25ed9e8d1d19c3740206704fd703e4 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 14 Jun 2023 12:47:00 +0200 Subject: [PATCH 03/11] simple room query --- backend/src/schema/resolvers/rooms.ts | 14 ++++++++++++-- backend/src/schema/types/type/Room.gql | 12 ++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index 6d64c49b3..11b2c9201 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -1,6 +1,16 @@ import { v4 as uuid } from 'uuid' +import { neo4jgraphql } from 'neo4j-graphql-js' export default { + Query: { + Room: async (object, params, context, resolveInfo) => { + if (!params.filter) params.filter = {} + params.filter.users_some = { + id: context.user.id, + } + return neo4jgraphql(object, params, context, resolveInfo) + }, + }, Mutation: { CreateRoom: async (_parent, params, context, _resolveInfo) => { const { userId } = params @@ -33,6 +43,6 @@ export default { } finally { session.close() } - } - } + }, + }, } diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql index 0127021cb..bedc8b6b4 100644 --- a/backend/src/schema/types/type/Room.gql +++ b/backend/src/schema/types/type/Room.gql @@ -1,3 +1,11 @@ +input _RoomFilter { + AND: [_RoomFilter!] + OR: [_RoomFilter!] + users: _UserFilter + users_in: [_UserFilter!] + users_some: [_UserFilter!] +} + type Room { id: ID! createdAt: String @@ -11,3 +19,7 @@ type Mutation { userId: ID! ): Room } + +type Query { + Room: [Room] +} From a51f3573570e7a7742b14cfa98894abbcbe0637f Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 14 Jun 2023 15:00:23 +0200 Subject: [PATCH 04/11] filter that only the rooms of the current user are returned --- backend/src/middleware/permissionsMiddleware.ts | 1 + backend/src/schema/resolvers/rooms.ts | 8 ++++++++ backend/src/schema/types/type/Room.gql | 6 ++---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index 6498bba2f..fbca8846d 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -406,6 +406,7 @@ export default shield( queryLocations: isAuthenticated, availableRoles: isAdmin, getInviteCode: isAuthenticated, // and inviteRegistration + Room: isAuthenticated, }, Mutation: { '*': deny, diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index 11b2c9201..fe3779293 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -1,5 +1,6 @@ import { v4 as uuid } from 'uuid' import { neo4jgraphql } from 'neo4j-graphql-js' +import Resolver from './helpers/Resolver' export default { Query: { @@ -45,4 +46,11 @@ export default { } }, }, + Room: { + ...Resolver('Room', { + hasMany: { + users: '<-[:CHATS_IN]-(related:User)', + } + }), + } } diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql index bedc8b6b4..b3b5ea913 100644 --- a/backend/src/schema/types/type/Room.gql +++ b/backend/src/schema/types/type/Room.gql @@ -1,9 +1,7 @@ input _RoomFilter { AND: [_RoomFilter!] OR: [_RoomFilter!] - users: _UserFilter - users_in: [_UserFilter!] - users_some: [_UserFilter!] + users_some: _UserFilter } type Room { @@ -11,7 +9,7 @@ type Room { createdAt: String updatedAt: String - users: [User!]! @relation(name: "CHATS_IN", direction: "IN") + users: [User]! @relation(name: "CHATS_IN", direction: "IN") } type Mutation { From 5b859bbcb5f7f21903f44d966436e0ea56ee51f8 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 14 Jun 2023 15:38:05 +0200 Subject: [PATCH 05/11] test roomms resolver --- backend/src/graphql/rooms.ts | 28 +++ backend/src/schema/resolvers/rooms.spec.ts | 198 +++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 backend/src/graphql/rooms.ts create mode 100644 backend/src/schema/resolvers/rooms.spec.ts diff --git a/backend/src/graphql/rooms.ts b/backend/src/graphql/rooms.ts new file mode 100644 index 000000000..38d10a1d8 --- /dev/null +++ b/backend/src/graphql/rooms.ts @@ -0,0 +1,28 @@ +import gql from 'graphql-tag' + +export const createRoomMutation = () => { + return gql` + mutation ( + $userId: ID! + ) { + CreateRoom( + userId: $userId + ) { + id + } + } + ` +} + +export const roomQuery = () => { + return gql` + query { + Room { + id + users { + id + } + } + } + ` +} diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts new file mode 100644 index 000000000..26c95920b --- /dev/null +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -0,0 +1,198 @@ +import { createTestClient } from 'apollo-server-testing' +import Factory, { cleanDatabase } from '../../db/factories' +import { getNeode, getDriver } from '../../db/neo4j' +import { createRoomMutation, roomQuery } from '../../graphql/rooms' +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('Room', () => { + 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 room', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + await expect(mutate({ mutation: createRoomMutation(), variables: { + userId: 'some-id' } })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + authenticatedUser = await chattingUser.toJson() + }) + + describe('user id does not exist', () => { + it('returns null', async () => { + await expect(mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'not-existing-user', + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + CreateRoom: null, + }, + }) + }) + }) + + describe('user id exists', () => { + it('returns the id of the room', async () => { + await expect(mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'other-chatting-user', + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + CreateRoom: { + id: expect.any(String), + }, + }, + }) + }) + }) + }) + }) + + describe('query room', () => { + describe('unauthenticated', () => { + beforeAll(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect(query({ query: roomQuery() })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + describe('as creater of room', () => { + beforeAll(async () => { + authenticatedUser = await chattingUser.toJson() + }) + + it('returns the room', async () => { + await expect(query({ query: roomQuery() })).resolves.toMatchObject({ + errors: undefined, + data: { + Room: [ + { + id: expect.any(String), + users: expect.arrayContaining([ + { + id: 'chatting-user', + }, + { + id: 'other-chatting-user', + }, + ]), + }, + ], + }, + }) + }) + }) + + describe('as chatter of room', () => { + beforeAll(async () => { + authenticatedUser = await otherChattingUser.toJson() + }) + + it('returns the room', async () => { + await expect(query({ query: roomQuery() })).resolves.toMatchObject({ + errors: undefined, + data: { + Room: [ + { + id: expect.any(String), + users: expect.arrayContaining([ + { + id: 'chatting-user', + }, + { + id: 'other-chatting-user', + }, + ]), + }, + ], + }, + }) + }) + }) + + describe('as not chatter of room', () => { + beforeAll(async () => { + authenticatedUser = await notChattingUser.toJson() + }) + + it('returns no rooms', async () => { + await expect(query({ query: roomQuery() })).resolves.toMatchObject({ + errors: undefined, + data: { + Room: [], + }, + }) + }) + }) + }) + }) +}) From 0cdbf17fbfe0d702d4479f13e2e495688db8ea3a Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Jun 2023 08:53:24 +0200 Subject: [PATCH 06/11] remove semicolon --- backend/src/schema/resolvers/rooms.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 26c95920b..7ebf7cd54 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -35,7 +35,7 @@ afterAll(async () => { describe('Room', () => { beforeAll(async () => { - ;[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([ + [chattingUser, otherChattingUser, notChattingUser] = await Promise.all([ Factory.build( 'user', { From 8063e58ef2d0c4b293d3e767244f5a4891ae1423 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Jun 2023 09:02:32 +0200 Subject: [PATCH 07/11] use apoc.create.uuid to set id --- backend/src/schema/resolvers/rooms.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index fe3779293..342bbe573 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -1,4 +1,3 @@ -import { v4 as uuid } from 'uuid' import { neo4jgraphql } from 'neo4j-graphql-js' import Resolver from './helpers/Resolver' @@ -16,20 +15,20 @@ export default { CreateRoom: async (_parent, params, context, _resolveInfo) => { const { userId } = params const { user: { id: currentUserId } } = context - const roomId = uuid() const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { const createRoomCypher = ` MATCH (currentUser:User { id: $currentUserId }) MATCH (user:User { id: $userId }) MERGE (currentUser)-[:CHATS_IN]->(room:Room)<-[:CHATS_IN]-(user) - SET room.createdAt = toString(datetime()), - room.id = $roomId + ON CREATE SET + room.createdAt = toString(datetime()), + room.id = apoc.create.uuid() RETURN room { .* } ` const createRommTxResponse = await await transaction.run( createRoomCypher, - { userId, currentUserId, roomId } + { userId, currentUserId } ) const [room] = await createRommTxResponse.records.map((record) => record.get('room'), From f86367e340b08f5d84b9116f3ee1b386b35498f5 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Jun 2023 09:09:27 +0200 Subject: [PATCH 08/11] fix typo, comment room filter --- backend/src/schema/resolvers/rooms.spec.ts | 2 +- backend/src/schema/types/type/Room.gql | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 7ebf7cd54..dd6bce156 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -125,7 +125,7 @@ describe('Room', () => { }) describe('authenticated', () => { - describe('as creater of room', () => { + describe('as creator of room', () => { beforeAll(async () => { authenticatedUser = await chattingUser.toJson() }) diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql index b3b5ea913..8859ccc29 100644 --- a/backend/src/schema/types/type/Room.gql +++ b/backend/src/schema/types/type/Room.gql @@ -1,8 +1,8 @@ -input _RoomFilter { - AND: [_RoomFilter!] - OR: [_RoomFilter!] - users_some: _UserFilter -} +# input _RoomFilter { +# AND: [_RoomFilter!] +# OR: [_RoomFilter!] +# users_some: _UserFilter +# } type Room { id: ID! From 66652cf74ec3d16ec36acc8c29d3e8a89c977b10 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Jun 2023 09:10:52 +0200 Subject: [PATCH 09/11] remove doubled await --- backend/src/schema/resolvers/rooms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index 342bbe573..005994fd5 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -26,7 +26,7 @@ export default { room.id = apoc.create.uuid() RETURN room { .* } ` - const createRommTxResponse = await await transaction.run( + const createRommTxResponse = await transaction.run( createRoomCypher, { userId, currentUserId } ) From 3a95e25e1c38e5d26cc97ecc952e1b478bc1ae8a Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Jun 2023 09:18:50 +0200 Subject: [PATCH 10/11] add test for room creation with existing user id --- backend/src/schema/resolvers/rooms.spec.ts | 26 ++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index dd6bce156..1c9333bd7 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -71,6 +71,8 @@ describe('Room', () => { }) describe('authenticated', () => { + let roomId: string + beforeAll(async () => { authenticatedUser = await chattingUser.toJson() }) @@ -93,12 +95,14 @@ describe('Room', () => { describe('user id exists', () => { it('returns the id of the room', async () => { - await expect(mutate({ + const result = await mutate({ mutation: createRoomMutation(), variables: { userId: 'other-chatting-user', }, - })).resolves.toMatchObject({ + }) + roomId = result.data.CreateRoom.id + await expect(result).toMatchObject({ errors: undefined, data: { CreateRoom: { @@ -108,6 +112,24 @@ describe('Room', () => { }) }) }) + + describe('create room with same user id', () => { + it('returns the id of the room', async () => { + await expect(mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'other-chatting-user', + }, + })).resolves.toMatchObject({ + errors: undefined, + data: { + CreateRoom: { + id: roomId, + }, + }, + }) + }) + }) }) }) From 85950843f79d6b48cf5e6d6d929657d8276ea68d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 20 Jun 2023 09:25:23 +0200 Subject: [PATCH 11/11] add spaces --- backend/src/schema/resolvers/rooms.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index 005994fd5..f3ea05cc9 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -22,8 +22,8 @@ export default { MATCH (user:User { id: $userId }) MERGE (currentUser)-[:CHATS_IN]->(room:Room)<-[:CHATS_IN]-(user) ON CREATE SET - room.createdAt = toString(datetime()), - room.id = apoc.create.uuid() + room.createdAt = toString(datetime()), + room.id = apoc.create.uuid() RETURN room { .* } ` const createRommTxResponse = await transaction.run(