From b9655acc124485d65333b3891e1674964911beea Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 29 Sep 2019 17:09:07 +0200 Subject: [PATCH] Implemented Request API - implemented sendRequest, acceptRequest, denyRequest --- src/lib/dataaccess/ChatMessage.ts | 7 +- src/lib/dataaccess/Post.ts | 2 +- src/lib/dataaccess/Profile.ts | 94 ++++++++++++++++++++++++-- src/lib/dataaccess/Request.ts | 12 ++++ src/lib/dataaccess/index.ts | 17 +++++ src/lib/errors/RequestNotFoundError.ts | 9 +++ src/public/graphql/schema.graphql | 24 +++---- src/routes/home.ts | 46 +++++++++++++ src/sql/create-tables.sql | 16 +++-- src/sql/update-tables.sql | 3 + 10 files changed, 206 insertions(+), 24 deletions(-) create mode 100644 src/lib/dataaccess/Request.ts create mode 100644 src/lib/errors/RequestNotFoundError.ts diff --git a/src/lib/dataaccess/ChatMessage.ts b/src/lib/dataaccess/ChatMessage.ts index 9ae0a12..bbad8ad 100644 --- a/src/lib/dataaccess/ChatMessage.ts +++ b/src/lib/dataaccess/ChatMessage.ts @@ -3,8 +3,11 @@ import {Chatroom} from "./Chatroom"; import {User} from "./User"; export class ChatMessage { - constructor(public author: User, public chat: Chatroom, public createdAt: number, public content: string) { - } + constructor( + public readonly author: User, + public readonly chat: Chatroom, + public readonly createdAt: number, + public readonly content: string) {} /** * The content rendered by markdown-it. diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts index 2d3d8d6..7908f62 100644 --- a/src/lib/dataaccess/Post.ts +++ b/src/lib/dataaccess/Post.ts @@ -108,7 +108,7 @@ export class Post extends DataObject { } else { if (uVote) { await queryHelper.first({ - text: "UPDATE votes SET vote_type = $1 WHERE user_id = $1 AND item_id = $3", + text: "UPDATE votes SET vote_type = $1 WHERE user_id = $2 AND item_id = $3", values: [type, userId, this.id], }); } else { diff --git a/src/lib/dataaccess/Profile.ts b/src/lib/dataaccess/Profile.ts index 957d631..090e6da 100644 --- a/src/lib/dataaccess/Profile.ts +++ b/src/lib/dataaccess/Profile.ts @@ -1,6 +1,8 @@ +import {RequestNotFoundError} from "../errors/RequestNotFoundError"; import {Chatroom} from "./Chatroom"; -import {queryHelper} from "./index"; +import dataaccess, {queryHelper} from "./index"; import {User} from "./User"; +import {Request} from "./Request"; export class Profile extends User { @@ -28,6 +30,30 @@ export class Profile extends User { } } + /** + * Returns all open requests the user has send. + */ + public async sentRequests() { + const result = await queryHelper.all({ + cache: true, + text: "SELECT * FROM requests WHERE sender = $1", + values: [this.id], + }); + return this.getRequests(result); + } + + /** + * Returns all received requests of the user. + */ + public async receivedRequests() { + const result = await queryHelper.all({ + cache: true, + text: "SELECT * FROM requests WHERE receiver = $1", + values: [this.id], + }); + return this.getRequests(result); + } + /** * Sets the greenpoints of a user. * @param points @@ -46,7 +72,7 @@ export class Profile extends User { */ public async setEmail(email: string): Promise { const result = await queryHelper.first({ - text: "UPDATE TABLE users SET email = $1 WHERE users.id = $2 RETURNING email", + text: "UPDATE users SET email = $1 WHERE users.id = $2 RETURNING email", values: [email, this.id], }); return result.email; @@ -57,7 +83,7 @@ export class Profile extends User { */ public async setHandle(handle: string): Promise { const result = await queryHelper.first({ - text: "UPDATE TABLE users SET handle = $1 WHERE id = $2", + text: "UPDATE users SET handle = $1 WHERE id = $2", values: [handle, this.id], }); return result.handle; @@ -69,9 +95,69 @@ export class Profile extends User { */ public async setName(name: string): Promise { const result = await queryHelper.first({ - text: "UPDATE TABLE users SET name = $1 WHERE id = $2", + text: "UPDATE users SET name = $1 WHERE id = $2", values: [name, this.id], }); return result.name; } + + /** + * Denys a request. + * @param sender + * @param type + */ + public async denyRequest(sender: number, type: dataaccess.RequestType) { + await queryHelper.first({ + text: "DELETE FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3", + values: [this.id, sender, type], + }); + } + + /** + * Accepts a request. + * @param sender + * @param type + */ + public async acceptRequest(sender: number, type: dataaccess.RequestType) { + const exists = await queryHelper.first({ + cache: true, + text: "SELECT 1 FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3", + values: [this.id, sender, type], + }); + if (exists) { + if (type === dataaccess.RequestType.FRIENDREQUEST) { + await queryHelper.first({ + text: "INSERT INTO user_friends (user_id, friend_id) VALUES ($1, $2)", + values: [this.id, sender], + }); + } + } else { + throw new RequestNotFoundError(sender, this.id, type); + } + } + + /** + * Returns request wrapper for a row database request result. + * @param rows + */ + private getRequests(rows: any) { + const requests = []; + const requestUsers: any = {}; + + for (const row of rows) { + let sender = requestUsers[row.sender]; + + if (!sender) { + sender = new User(row.sender); + requestUsers[row.sender] = sender; + } + let receiver = requestUsers[row.receiver]; + if (!receiver) { + receiver = new User(row.receiver); + requestUsers[row.receiver] = receiver; + } + requests.push(new Request(sender, receiver, row.type)); + } + return requests; + } } diff --git a/src/lib/dataaccess/Request.ts b/src/lib/dataaccess/Request.ts new file mode 100644 index 0000000..f161757 --- /dev/null +++ b/src/lib/dataaccess/Request.ts @@ -0,0 +1,12 @@ +import dataaccess from "./index"; +import {User} from "./User"; + +/** + * Represents a request to a user. + */ +export class Request { + constructor( + public readonly sender: User, + public readonly receiver: User, + public readonly type: dataaccess.RequestType) {} +} diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 039d3d4..28ae27c 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -7,6 +7,7 @@ import {ChatMessage} from "./ChatMessage"; import {Chatroom} from "./Chatroom"; import {Post} from "./Post"; import {Profile} from "./Profile"; +import {Request} from "./Request"; import {User} from "./User"; const config = globals.config; @@ -184,6 +185,22 @@ namespace dataaccess { } } + /** + * Sends a request to a user. + * @param sender + * @param receiver + * @param type + */ + export async function createRequest(sender: number, receiver: number, type?: RequestType) { + type = type || RequestType.FRIENDREQUEST; + + const result = await queryHelper.first({ + text: "INSERT INTO requests (sender, receiver, type) VALUES ($1, $2, $3) RETURNING *", + values: [sender, receiver, type], + }); + return new Request(new User(result.sender), new User(result.receiver), result.type); + } + /** * Enum representing the types of votes that can be performed on a post. */ diff --git a/src/lib/errors/RequestNotFoundError.ts b/src/lib/errors/RequestNotFoundError.ts new file mode 100644 index 0000000..8a020d1 --- /dev/null +++ b/src/lib/errors/RequestNotFoundError.ts @@ -0,0 +1,9 @@ +import dataaccess from "../dataaccess"; +import {BaseError} from "./BaseError"; + +export class RequestNotFoundError extends BaseError { + constructor(sender: number, receiver: number, type: dataaccess.RequestType) { + super(`Request with sender '${sender}' and receiver '${receiver}' of type '${type}' not found.`); + } + +} diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index 1629f16..19a19eb 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -11,9 +11,6 @@ type Query { "returns the chat object for a chat id" getChat(chatId: ID!): ChatRoom - "returns the request object for a request id" - getRequest(requestId: ID!): Request - "find a post by the posted date or content" findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post] @@ -41,10 +38,10 @@ type Mutation { report(postId: ID!): Boolean "send a request" - sendRequest(reciever: ID!, type: RequestType): Boolean + sendRequest(receiver: ID!, type: RequestType): Request "lets you accept a request for a given request id" - acceptRequest(requestId: ID!): Boolean + acceptRequest(sender: ID!, type: RequestType): Boolean "lets you deny a request for a given request id" denyRequest(requestId: ID!): Boolean @@ -143,8 +140,11 @@ type Profile implements UserData { "all friends of the user" friends: [User] - "all request for groupChats/friends/events" - requests: [Request] + "all sent request for groupChats/friends/events" + sentRequests: [Request] + + "all received request for groupChats/friends/events" + receivedRequests: [Request] } "represents a single user post" @@ -177,9 +177,6 @@ type Post { "represents a request of any type" type Request { - "id of the request" - id: ID! - "Id of the user who sended the request" sender: User! @@ -187,7 +184,7 @@ type Request { receiver: User! "type of the request" - requestType: RequestType! + type: RequestType! } "represents a chatroom" @@ -225,7 +222,10 @@ enum VoteType { DOWNVOTE } -"represents the type of request that the user has received" +""" +represents the type of request that the user has received +Currently on Friend Requests are implemented. +""" enum RequestType { FRIENDREQUEST GROUPINVITE diff --git a/src/routes/home.ts b/src/routes/home.ts index 97e5943..88ce8b0 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -183,6 +183,7 @@ class HomeRoute extends Route { }, async sendMessage({chatId, content}: {chatId: number, content: string}) { if (!req.session.userId) { + res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); } if (chatId && content) { @@ -197,6 +198,51 @@ class HomeRoute extends Route { return new GraphQLError("No chatId or content given."); } }, + async sendRequest({receiver, type}: {receiver: number, type: dataaccess.RequestType}) { + if (!req.session.userId) { + res.status(status.UNAUTHORIZED); + return new NotLoggedInGqlError(); + } + if (receiver && type) { + return await dataaccess.createRequest(req.session.userId, receiver, type); + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No receiver or type given."); + } + }, + async denyRequest({sender, type}: {sender: number, type: dataaccess.RequestType}) { + if (!req.session.userId) { + res.status(status.UNAUTHORIZED); + return new NotLoggedInGqlError(); + } + if (sender && type) { + const profile = new Profile(req.session.userId); + await profile.denyRequest(sender, type); + return true; + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No sender or type given."); + } + }, + async acceptRequest({sender, type}: {sender: number, type: dataaccess.RequestType}) { + if (!req.session.userId) { + res.status(status.UNAUTHORIZED); + return new NotLoggedInGqlError(); + } + if (sender && type) { + try { + const profile = new Profile(req.session.userId); + await profile.acceptRequest(sender, type); + return true; + } catch (err) { + res.status(status.BAD_REQUEST); + return err.graphqlError; + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No sender or type given."); + } + }, }; } } diff --git a/src/sql/create-tables.sql b/src/sql/create-tables.sql index 486f50e..128492b 100644 --- a/src/sql/create-tables.sql +++ b/src/sql/create-tables.sql @@ -81,7 +81,8 @@ DO $$ BEGIN CREATE TABLE IF NOT EXISTS votes ( user_id SERIAL REFERENCES users (id) ON DELETE CASCADE, item_id BIGSERIAL REFERENCES posts (id) ON DELETE CASCADE, - vote_type votetype DEFAULT 'DOWNVOTE' + vote_type votetype DEFAULT 'DOWNVOTE', + PRIMARY KEY (user_id, item_id) ); CREATE TABLE IF NOT EXISTS events ( @@ -92,7 +93,8 @@ DO $$ BEGIN CREATE TABLE IF NOT EXISTS event_members ( event BIGSERIAL REFERENCES events (id), - member SERIAL REFERENCES users (id) + member SERIAL REFERENCES users (id), + PRIMARY KEY (event, member) ); CREATE TABLE IF NOT EXISTS chats ( @@ -109,17 +111,21 @@ DO $$ BEGIN CREATE TABLE IF NOT EXISTS chat_members ( chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE, - member SERIAL REFERENCES users (id) ON DELETE CASCADE + member SERIAL REFERENCES users (id) ON DELETE CASCADE, + PRIMARY KEY (chat, member) ); CREATE TABLE IF NOT EXISTS user_friends ( user_id SERIAL REFERENCES users (id) ON DELETE CASCADE, - friend_id SERIAL REFERENCES users (id) ON DELETE CASCADE + friend_id SERIAL REFERENCES users (id) ON DELETE CASCADE, + PRIMARY KEY (user_id, friend_id) ); CREATE TABLE IF NOT EXISTS requests ( sender SERIAL REFERENCES users (id) ON DELETE CASCADE, - receiver SERIAL REFERENCES users (id) ON DELETE CASCADE + receiver SERIAL REFERENCES users (id) ON DELETE CASCADE, + type requesttype DEFAULT 'FRIENDREQUEST', + PRIMARY KEY (sender, receiver, type) ); END $$; diff --git a/src/sql/update-tables.sql b/src/sql/update-tables.sql index da794c7..858a214 100644 --- a/src/sql/update-tables.sql +++ b/src/sql/update-tables.sql @@ -13,4 +13,7 @@ DO $$ BEGIN DROP COLUMN IF EXISTS upvotes, DROP COLUMN IF EXISTS downvotes; + ALTER TABLE requests + ADD COLUMN IF NOT EXISTS type requesttype DEFAULT 'FRIENDREQUEST'; + END $$;