From 1d97e3305efc8a87dd815b8d267235d16e852aca Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 24 Sep 2019 18:06:04 +0200 Subject: [PATCH] API implementation --- package-lock.json | 41 +++++++++++++++++------- src/lib/QueryHelper.ts | 2 +- src/lib/dataaccess/DataObject.ts | 6 ++-- src/lib/dataaccess/Post.ts | 52 ++++++++++++++++++++++++------- src/lib/dataaccess/User.ts | 31 ++++++++++++------ src/lib/dataaccess/index.ts | 15 +++++++++ src/public/graphql/schema.graphql | 19 ++++++----- src/routes/home.ts | 38 ++++++++++++++++++++++ src/sql/create-tables.sql | 2 +- src/sql/update-tables.sql | 9 ++++-- 10 files changed, 169 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3689e96..64e04b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2532,7 +2532,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2553,12 +2554,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2573,17 +2576,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2700,7 +2706,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2712,6 +2719,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2726,6 +2734,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2733,12 +2742,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2757,6 +2768,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2837,7 +2849,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2849,6 +2862,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2934,7 +2948,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2970,6 +2985,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2989,6 +3005,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3032,12 +3049,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/src/lib/QueryHelper.ts b/src/lib/QueryHelper.ts index 5970f70..95b7a55 100644 --- a/src/lib/QueryHelper.ts +++ b/src/lib/QueryHelper.ts @@ -133,7 +133,7 @@ export class QueryHelper { try { return await this.pool.query(query); } catch (err) { - logger.debug(`Error on query "${query}".`); + logger.debug(`Error on query "${JSON.stringify(query)}".`); logger.error(`Sql query failed: ${err}`); logger.verbose(err.stack); return { diff --git a/src/lib/dataaccess/DataObject.ts b/src/lib/dataaccess/DataObject.ts index 1890425..019bcc3 100644 --- a/src/lib/dataaccess/DataObject.ts +++ b/src/lib/dataaccess/DataObject.ts @@ -12,9 +12,9 @@ export abstract class DataObject { /** * Loads data from the database if data has not been loaded */ - protected loadDataIfNotExists() { - if (this.dataLoaded) { - this.loadData(); + protected async loadDataIfNotExists() { + if (!this.dataLoaded) { + await this.loadData(); } } } diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts index b4193c4..08f9c4b 100644 --- a/src/lib/dataaccess/Post.ts +++ b/src/lib/dataaccess/Post.ts @@ -5,8 +5,6 @@ import {User} from "./User"; export class Post extends DataObject { public readonly id: number; - private $upvotes: number; - private $downvotes: number; private $createdAt: string; private $content: string; private $author: number; @@ -16,23 +14,29 @@ export class Post extends DataObject { * Returns the upvotes of a post. */ public async upvotes(): Promise { - this.loadDataIfNotExists(); - return this.$upvotes; + const result = await queryHelper.first({ + text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'UPVOTE'", + values: [this.id], + }); + return result.count; } /** * Returns the downvotes of the post */ public async downvotes(): Promise { - this.loadDataIfNotExists(); - return this.$downvotes; + const result = await queryHelper.first({ + text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'DOWNVOTE'", + values: [this.id], + }); + return result.count; } /** * The content of the post (markdown) */ public async content(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$content; } @@ -40,7 +44,7 @@ export class Post extends DataObject { * The date the post was created at. */ public async createdAt(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$createdAt; } @@ -48,7 +52,7 @@ export class Post extends DataObject { * The autor of the post. */ public async author(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return new User(this.$author); } @@ -77,6 +81,34 @@ export class Post extends DataObject { } } + /** + * Performs a vote on a post. + * @param userId + * @param type + */ + public async vote(userId: number, type: dataaccess.VoteType): Promise { + const uVote = await this.userVote(userId); + if (uVote === type) { + await queryHelper.first({ + text: "DELETE FROM votes WHERE item_id = $1 AND user_id = $2", + values: [this.id, userId], + }); + } else { + if (uVote) { + await queryHelper.first({ + text: "UPDATE votes SET vote_type = $1 WHERE user_id = $1 AND item_id = $3", + values: [type, userId, this.id], + }); + } else { + await queryHelper.first({ + text: "INSERT INTO votes (user_id, item_id, vote_type) values ($1, $2, $3)", + values: [userId, this.id, type], + }); + } + return type; + } + } + /** * Loads the data from the database if needed. */ @@ -93,8 +125,6 @@ export class Post extends DataObject { if (result) { this.$author = result.author; this.$content = result.content; - this.$downvotes = result.downvotes; - this.$upvotes = result.upvotes; this.$createdAt = result.created_at; this.$type = result.type; this.dataLoaded = true; diff --git a/src/lib/dataaccess/User.ts b/src/lib/dataaccess/User.ts index 394c062..de5b25e 100644 --- a/src/lib/dataaccess/User.ts +++ b/src/lib/dataaccess/User.ts @@ -13,7 +13,7 @@ export class User extends DataObject { * The name of the user */ public async name(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$name; } @@ -21,7 +21,7 @@ export class User extends DataObject { * The unique handle of the user. */ public async handle(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$handle; } @@ -29,7 +29,7 @@ export class User extends DataObject { * The email of the user */ public async email(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$email; } @@ -37,25 +37,38 @@ export class User extends DataObject { * The number of greenpoints of the user */ public async greenpoints(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$greenpoints; } + /** + * Returns the number of posts the user created + */ + public async numberOfPosts(): Promise { + const result = await queryHelper.first({ + text: "SELECT COUNT(*) count FROM posts WHERE author = $1", + values: [this.id], + }); + return result.count; + } + /** * The date the user joined the platform */ public async joinedAt(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return new Date(this.$joinedAt); } /** * Returns all posts for a user. */ - public async posts(): Promise { + public async posts({first, offset}: {first: number, offset: number}): Promise { + first = first || 10; + offset = offset || 0; const result = await queryHelper.all({ - text: "SELECT * FROM posts WHERE author = $1", - values: [this.id], + text: "SELECT * FROM posts WHERE author = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", + values: [this.id, first, offset], }); const posts = []; @@ -74,7 +87,7 @@ export class User extends DataObject { result = this.row; } else { result = await queryHelper.first({ - text: "SELECT * FROM users WHERE user.id = $1", + text: "SELECT * FROM users WHERE users.id = $1", values: [this.id], }); } diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 6a9c854..a0e38f5 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 {Post} from "./Post"; import {Profile} from "./Profile"; import {User} from "./User"; @@ -88,6 +89,20 @@ namespace dataaccess { return new Profile(result.id, result); } + /** + * Creates a post + * @param content + * @param authorId + * @param type + */ + export async function createPost(content: string, authorId: number, type: string) { + const result = await queryHelper.first({ + text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *", + values: [content, authorId, type], + }); + return new Post(result.id, result); + } + /** * Enum representing the types of votes that can be performed on a post. */ diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index ea9c4eb..eba344a 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -2,6 +2,9 @@ type Query { "returns the user object for a given user id" getUser(userId: ID): User + "returns the logged in user" + getSelf: User + "returns the post object for a post id" getPost(postId: ID): Post @@ -25,11 +28,14 @@ type Mutation { "Login of the user. The passwordHash should be a sha512 hash of the password." login(email: String, passwordHash: String): User + "Registers the user." + register(username: String, email: String, passwordHash: String): User + "Logout of the user." logout: Boolean "Upvote/downvote a Post" - vote(postId: ID!, type: [VoteType!]!): Boolean + vote(postId: ID!, type: VoteType!): VoteType "Report the post" report(postId: ID!): Boolean @@ -47,7 +53,7 @@ type Mutation { sendMessage(chatId: ID!, content: String!): Boolean "create the post" - createPost(text: String, picture: String, tags: [String]): Boolean + createPost(content: String!): Boolean "delete the post for a given post id" deletePost(postId: ID!): Boolean @@ -56,7 +62,7 @@ type Mutation { "represents a single user account" type User { "url for the Profile picture of the User" - profilePicture: String! + profilePicture: String "name of the User" name: String! @@ -71,13 +77,10 @@ type User { numberOfPosts: Int "returns a given number of posts of a user" - getAllPosts(first: Int=10, offset: Int): [Post] + posts(first: Int=10, offset: Int): [Post] "creation date of the user account" - joinedDate: String! - - "returns chats the user pinned" - pinnedChats: [ChatRoom] + joinedAt: String! "returns all friends of the user" friends: [User] diff --git a/src/routes/home.ts b/src/routes/home.ts index 2af5947..1ed5115 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -4,6 +4,8 @@ import * as status from "http-status"; import {constants} from "http2"; import {Server} from "socket.io"; import dataaccess from "../lib/dataaccess"; +import {Post} from "../lib/dataaccess/Post"; +import {Profile} from "../lib/dataaccess/Profile"; import Route from "../lib/Route"; /** @@ -42,6 +44,14 @@ class HomeRoute extends Route { */ public resolver(req: any, res: any): any { return { + getSelf() { + if (req.session.userId) { + return new Profile(req.session.userId); + } else { + res.status(status.UNAUTHORIZED); + return new GraphQLError("Not logged in"); + } + }, acceptCookies() { req.session.cookiesAccepted = true; return true; @@ -70,6 +80,34 @@ class HomeRoute extends Route { return new GraphQLError("User is not logged in."); } }, + async register(args: any) { + if (args.username && args.email && args.passwordHash) { + const user = await dataaccess.registerUser(args.username, args.email, args.passwordHash); + if (user) { + req.session.userId = user.id; + return user; + } else { + res.status(status.INTERNAL_SERVER_ERROR); + return new GraphQLError("Failed to create account."); + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No username, email or password given."); + } + }, + async vote(args: any) { + if (args.postId && args.type) { + if (req.session.userId) { + return await (new Post(args.postId)).vote(req.session.userId, args.type); + } else { + res.status(status.UNAUTHORIZED); + return new GraphQLError("Not logged in."); + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No postId or type given."); + } + }, }; } diff --git a/src/sql/create-tables.sql b/src/sql/create-tables.sql index faf2137..17f6e74 100644 --- a/src/sql/create-tables.sql +++ b/src/sql/create-tables.sql @@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS posts ( created_at TIMESTAMP DEFAULT now(), content text, author SERIAL REFERENCES users (id) ON DELETE CASCADE, - type varchar(16) NOT NULL + type varchar(16) NOT NULL DEFAULT 'MISC' ); CREATE TABLE IF NOT EXISTS votes ( diff --git a/src/sql/update-tables.sql b/src/sql/update-tables.sql index 52ce2f6..13bc681 100644 --- a/src/sql/update-tables.sql +++ b/src/sql/update-tables.sql @@ -1,3 +1,8 @@ ALTER TABLE IF EXISTS votes - ADD COLUMN IF NOT EXISTS vote_type varchar(8) DEFAULT 'upvote', - ALTER COLUMN vote_type SET DEFAULT 'upvote'; + ADD COLUMN IF NOT EXISTS vote_type varchar(8) DEFAULT 'UPVOTE', + ALTER COLUMN vote_type SET DEFAULT 'UPVOTE'; + +ALTER TABLE IF EXISTS posts + ALTER COLUMN type SET DEFAULT 'MISC', + DROP COLUMN IF EXISTS upvotes, + DROP COLUMN IF EXISTS downvotes;