From d7f819e02e18ee73395efe9b1f44d0f46a180583 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Fri, 27 Sep 2019 20:48:16 +0200 Subject: [PATCH] More graphql implementation stuff - implemented methods to get user and post information - implemented methods to create chatrooms --- package-lock.json | 41 +++++-------------- src/app.ts | 2 +- src/lib/QueryHelper.ts | 2 +- src/lib/dataaccess/index.ts | 46 +++++++++++++++++++++ src/lib/regex.ts | 11 +++++ src/public/graphql/schema.graphql | 13 +++--- src/routes/home.ts | 68 +++++++++++++++++++++++-------- 7 files changed, 130 insertions(+), 53 deletions(-) create mode 100644 src/lib/regex.ts diff --git a/package-lock.json b/package-lock.json index 34608d6..4d87280 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2532,8 +2532,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -2554,14 +2553,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2576,20 +2573,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -2706,8 +2700,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -2719,7 +2712,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2734,7 +2726,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2742,14 +2733,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2768,7 +2757,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2849,8 +2837,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -2862,7 +2849,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -2948,8 +2934,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -2985,7 +2970,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3005,7 +2989,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3049,14 +3032,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, diff --git a/src/app.ts b/src/app.ts index 7ea9b9b..5801771 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,7 +10,7 @@ import {importSchema} from "graphql-import"; import * as http from "http"; import * as path from "path"; import * as socketIo from "socket.io"; -import dataaccess from "./lib/dataaccess"; +import dataaccess, {queryHelper} from "./lib/dataaccess"; import globals from "./lib/globals"; import routes from "./routes"; diff --git a/src/lib/QueryHelper.ts b/src/lib/QueryHelper.ts index 95b7a55..fb1b638 100644 --- a/src/lib/QueryHelper.ts +++ b/src/lib/QueryHelper.ts @@ -54,7 +54,7 @@ export class SqlTransaction { /** * Releases the client back to the pool. */ - public async release() { + public release() { this.client.release(); } } diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index c0816e6..db72696 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -1,6 +1,7 @@ import {Pool} from "pg"; import globals from "../globals"; import {QueryHelper} from "../QueryHelper"; +import {Chatroom} from "./Chatroom"; import {Post} from "./Post"; import {Profile} from "./Profile"; import {User} from "./User"; @@ -89,6 +90,22 @@ namespace dataaccess { return new Profile(result.id, result); } + /** + * Returns a post for a given postId.s + * @param postId + */ + export async function getPost(postId: number) { + const result = await queryHelper.first({ + text: "SELECT * FROM posts WHERE id = $1", + values: [postId], + }); + if (result) { + return new Post(result.id, result); + } else { + return null; + } + } + /** * Creates a post * @param content @@ -115,6 +132,35 @@ namespace dataaccess { return true; } + /** + * Creates a chatroom containing two users + * @param members + */ + export async function createChat(...members: number[]) { + const idResult = await queryHelper.first({ + text: "INSERT INTO chats (id) values (nextval('chats_id_seq'::regclass)) RETURNING *;", + }); + const id = idResult.id; + const transaction = await queryHelper.createTransaction(); + try { + await transaction.begin(); + for (const member of members) { + await transaction.query({ + name: "chat-member-insert", + text: "INSERT INTO chat_members (ABSOLUTE chat, member) VALUES ($1, $2);", + values: [member], + }); + } + await transaction.commit(); + } catch (err) { + globals.logger.warn(`Failed to insert chatmember into database: ${err.message}`); + globals.logger.debug(err.stack); + } finally { + transaction.release(); + } + return new Chatroom(id); + } + /** * Enum representing the types of votes that can be performed on a post. */ diff --git a/src/lib/regex.ts b/src/lib/regex.ts new file mode 100644 index 0000000..1ed6c11 --- /dev/null +++ b/src/lib/regex.ts @@ -0,0 +1,11 @@ +export namespace is { + const emailRegex = /\S+?@\S+?(\.\S+?)?\.\w{2,3}(.\w{2-3})?/g + + /** + * Tests if a string is a valid email. + * @param testString + */ + export function email(testString: string) { + return emailRegex.test(testString) + } +} diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index bb369f5..60c32e3 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -1,18 +1,21 @@ type Query { - "returns the user object for a given user id" - getUser(userId: ID): User + "returns the user object for a given user id or a handle (only one required)" + getUser(userId: ID, handle: String): User "returns the logged in user" getSelf: User "returns the post object for a post id" - getPost(postId: ID): Post + getPost(postId: ID!): Post "returns the chat object for a chat id" - getChat(chatId: ID): ChatRoom + getChat(chatId: ID!): ChatRoom + + "Creates a chat between the user (and optional an other user)" + createChat(members: [ID!]): ChatRoom "returns the request object for a request id" - getRequest(requestId: ID): Request + getRequest(requestId: ID!): Request "find a post by the posted date or content" findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post] diff --git a/src/routes/home.ts b/src/routes/home.ts index 2a88931..1bb397b 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -5,6 +5,7 @@ import {Server} from "socket.io"; import dataaccess from "../lib/dataaccess"; import {Post} from "../lib/dataaccess/Post"; import {Profile} from "../lib/dataaccess/Profile"; +import {is} from "../lib/regex"; import Route from "../lib/Route"; /** @@ -50,13 +51,31 @@ class HomeRoute extends Route { return new GraphQLError("Not logged in"); } }, + async getUser({userId, handle}: {userId: number, handle: string}) { + if (handle) { + return await dataaccess.getUserByHandle(handle); + } else if (userId) { + return dataaccess.getUser(userId); + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No userId or handle provided."); + } + }, + async getPost({postId}: {postId: number}) { + if (postId) { + return await dataaccess.getPost(postId); + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No postId given."); + } + }, acceptCookies() { req.session.cookiesAccepted = true; return true; }, - async login(args: {email: string, passwordHash: string}) { - if (args.email && args.passwordHash) { - const user = await dataaccess.getUserByLogin(args.email, args.passwordHash); + async login({email, passwordHash}: {email: string, passwordHash: string}) { + if (email && passwordHash) { + const user = await dataaccess.getUserByLogin(email, passwordHash); if (user && user.id) { req.session.userId = user.id; return user; @@ -75,12 +94,16 @@ class HomeRoute extends Route { return true; } else { res.status(status.UNAUTHORIZED); - return new GraphQLError("User is not logged in."); + return new GraphQLError("Not logged in."); } }, - async register(args: {username: string, email: string, passwordHash: string}) { - if (args.username && args.email && args.passwordHash) { - const user = await dataaccess.registerUser(args.username, args.email, args.passwordHash); + async register({username, email, passwordHash}: {username: string, email: string, passwordHash: string}) { + if (username && email && passwordHash) { + if (!is.email(email)) { + res.status(status.BAD_REQUEST); + return new GraphQLError(`'${email}' is not a valid email address!`); + } + const user = await dataaccess.registerUser(username, email, passwordHash); if (user) { req.session.userId = user.id; return user; @@ -93,10 +116,10 @@ class HomeRoute extends Route { return new GraphQLError("No username, email or password given."); } }, - async vote(args: {postId: number, type: dataaccess.VoteType}) { - if (args.postId && args.type) { + async vote({postId, type}: {postId: number, type: dataaccess.VoteType}) { + if (postId && type) { if (req.session.userId) { - return await (new Post(args.postId)).vote(req.session.userId, args.type); + return await (new Post(postId)).vote(req.session.userId, type); } else { res.status(status.UNAUTHORIZED); return new GraphQLError("Not logged in."); @@ -106,10 +129,10 @@ class HomeRoute extends Route { return new GraphQLError("No postId or type given."); } }, - async createPost(args: {content: string}) { - if (args.content) { + async createPost({content}: {content: string}) { + if (content) { if (req.session.userId) { - return await dataaccess.createPost(args.content, req.session.userId); + return await dataaccess.createPost(content, req.session.userId); } else { res.status(status.UNAUTHORIZED); return new GraphQLError("Not logged in."); @@ -119,9 +142,9 @@ class HomeRoute extends Route { return new GraphQLError("Can't create empty post."); } }, - async deletePost(args: {postId: number}) { - if (args.postId) { - const post = new Post(args.postId); + async deletePost({postId}: {postId: number}) { + if (postId) { + const post = new Post(postId); if ((await post.author()).id === req.session.userId) { return await dataaccess.deletePost(post.id); } else { @@ -132,6 +155,19 @@ class HomeRoute extends Route { return new GraphQLError("No postId given."); } }, + async createChat({members}: {members: number[]}) { + if (req.session.userId) { + const chatMembers = [req.session.userId]; + if (members) { + chatMembers.push(...members); + } + return await dataaccess.createChat(...chatMembers); + + } else { + res.status(status.UNAUTHORIZED); + return new GraphQLError("Not logged in."); + } + }, }; } }