API implementation

pull/1/head
Trivernis 5 years ago
parent e6d2191266
commit 1d97e3305e

41
package-lock.json generated

@ -2532,7 +2532,8 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -2553,12 +2554,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -2573,17 +2576,20 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -2700,7 +2706,8 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -2712,6 +2719,7 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -2726,6 +2734,7 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -2733,12 +2742,14 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -2757,6 +2768,7 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -2837,7 +2849,8 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -2849,6 +2862,7 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -2934,7 +2948,8 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -2970,6 +2985,7 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -2989,6 +3005,7 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -3032,12 +3049,14 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
} }
} }
}, },

@ -133,7 +133,7 @@ export class QueryHelper {
try { try {
return await this.pool.query(query); return await this.pool.query(query);
} catch (err) { } catch (err) {
logger.debug(`Error on query "${query}".`); logger.debug(`Error on query "${JSON.stringify(query)}".`);
logger.error(`Sql query failed: ${err}`); logger.error(`Sql query failed: ${err}`);
logger.verbose(err.stack); logger.verbose(err.stack);
return { return {

@ -12,9 +12,9 @@ export abstract class DataObject {
/** /**
* Loads data from the database if data has not been loaded * Loads data from the database if data has not been loaded
*/ */
protected loadDataIfNotExists() { protected async loadDataIfNotExists() {
if (this.dataLoaded) { if (!this.dataLoaded) {
this.loadData(); await this.loadData();
} }
} }
} }

@ -5,8 +5,6 @@ import {User} from "./User";
export class Post extends DataObject { export class Post extends DataObject {
public readonly id: number; public readonly id: number;
private $upvotes: number;
private $downvotes: number;
private $createdAt: string; private $createdAt: string;
private $content: string; private $content: string;
private $author: number; private $author: number;
@ -16,23 +14,29 @@ export class Post extends DataObject {
* Returns the upvotes of a post. * Returns the upvotes of a post.
*/ */
public async upvotes(): Promise<number> { public async upvotes(): Promise<number> {
this.loadDataIfNotExists(); const result = await queryHelper.first({
return this.$upvotes; 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 * Returns the downvotes of the post
*/ */
public async downvotes(): Promise<number> { public async downvotes(): Promise<number> {
this.loadDataIfNotExists(); const result = await queryHelper.first({
return this.$downvotes; 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) * The content of the post (markdown)
*/ */
public async content(): Promise<string> { public async content(): Promise<string> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$content; return this.$content;
} }
@ -40,7 +44,7 @@ export class Post extends DataObject {
* The date the post was created at. * The date the post was created at.
*/ */
public async createdAt(): Promise<string> { public async createdAt(): Promise<string> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$createdAt; return this.$createdAt;
} }
@ -48,7 +52,7 @@ export class Post extends DataObject {
* The autor of the post. * The autor of the post.
*/ */
public async author(): Promise<User> { public async author(): Promise<User> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return new User(this.$author); 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<dataaccess.VoteType> {
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. * Loads the data from the database if needed.
*/ */
@ -93,8 +125,6 @@ export class Post extends DataObject {
if (result) { if (result) {
this.$author = result.author; this.$author = result.author;
this.$content = result.content; this.$content = result.content;
this.$downvotes = result.downvotes;
this.$upvotes = result.upvotes;
this.$createdAt = result.created_at; this.$createdAt = result.created_at;
this.$type = result.type; this.$type = result.type;
this.dataLoaded = true; this.dataLoaded = true;

@ -13,7 +13,7 @@ export class User extends DataObject {
* The name of the user * The name of the user
*/ */
public async name(): Promise<string> { public async name(): Promise<string> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$name; return this.$name;
} }
@ -21,7 +21,7 @@ export class User extends DataObject {
* The unique handle of the user. * The unique handle of the user.
*/ */
public async handle(): Promise<string> { public async handle(): Promise<string> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$handle; return this.$handle;
} }
@ -29,7 +29,7 @@ export class User extends DataObject {
* The email of the user * The email of the user
*/ */
public async email(): Promise<string> { public async email(): Promise<string> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$email; return this.$email;
} }
@ -37,25 +37,38 @@ export class User extends DataObject {
* The number of greenpoints of the user * The number of greenpoints of the user
*/ */
public async greenpoints(): Promise<number> { public async greenpoints(): Promise<number> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$greenpoints; return this.$greenpoints;
} }
/**
* Returns the number of posts the user created
*/
public async numberOfPosts(): Promise<number> {
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 * The date the user joined the platform
*/ */
public async joinedAt(): Promise<Date> { public async joinedAt(): Promise<Date> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return new Date(this.$joinedAt); return new Date(this.$joinedAt);
} }
/** /**
* Returns all posts for a user. * Returns all posts for a user.
*/ */
public async posts(): Promise<Post[]> { public async posts({first, offset}: {first: number, offset: number}): Promise<Post[]> {
first = first || 10;
offset = offset || 0;
const result = await queryHelper.all({ const result = await queryHelper.all({
text: "SELECT * FROM posts WHERE author = $1", text: "SELECT * FROM posts WHERE author = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
values: [this.id], values: [this.id, first, offset],
}); });
const posts = []; const posts = [];
@ -74,7 +87,7 @@ export class User extends DataObject {
result = this.row; result = this.row;
} else { } else {
result = await queryHelper.first({ result = await queryHelper.first({
text: "SELECT * FROM users WHERE user.id = $1", text: "SELECT * FROM users WHERE users.id = $1",
values: [this.id], values: [this.id],
}); });
} }

@ -1,6 +1,7 @@
import {Pool} from "pg"; import {Pool} from "pg";
import globals from "../globals"; import globals from "../globals";
import {QueryHelper} from "../QueryHelper"; import {QueryHelper} from "../QueryHelper";
import {Post} from "./Post";
import {Profile} from "./Profile"; import {Profile} from "./Profile";
import {User} from "./User"; import {User} from "./User";
@ -88,6 +89,20 @@ namespace dataaccess {
return new Profile(result.id, result); 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. * Enum representing the types of votes that can be performed on a post.
*/ */

@ -2,6 +2,9 @@ type Query {
"returns the user object for a given user id" "returns the user object for a given user id"
getUser(userId: ID): User getUser(userId: ID): User
"returns the logged in user"
getSelf: User
"returns the post object for a post id" "returns the post object for a post id"
getPost(postId: ID): Post 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 of the user. The passwordHash should be a sha512 hash of the password."
login(email: String, passwordHash: String): User login(email: String, passwordHash: String): User
"Registers the user."
register(username: String, email: String, passwordHash: String): User
"Logout of the user." "Logout of the user."
logout: Boolean logout: Boolean
"Upvote/downvote a Post" "Upvote/downvote a Post"
vote(postId: ID!, type: [VoteType!]!): Boolean vote(postId: ID!, type: VoteType!): VoteType
"Report the post" "Report the post"
report(postId: ID!): Boolean report(postId: ID!): Boolean
@ -47,7 +53,7 @@ type Mutation {
sendMessage(chatId: ID!, content: String!): Boolean sendMessage(chatId: ID!, content: String!): Boolean
"create the post" "create the post"
createPost(text: String, picture: String, tags: [String]): Boolean createPost(content: String!): Boolean
"delete the post for a given post id" "delete the post for a given post id"
deletePost(postId: ID!): Boolean deletePost(postId: ID!): Boolean
@ -56,7 +62,7 @@ type Mutation {
"represents a single user account" "represents a single user account"
type User { type User {
"url for the Profile picture of the User" "url for the Profile picture of the User"
profilePicture: String! profilePicture: String
"name of the User" "name of the User"
name: String! name: String!
@ -71,13 +77,10 @@ type User {
numberOfPosts: Int numberOfPosts: Int
"returns a given number of posts of a user" "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" "creation date of the user account"
joinedDate: String! joinedAt: String!
"returns chats the user pinned"
pinnedChats: [ChatRoom]
"returns all friends of the user" "returns all friends of the user"
friends: [User] friends: [User]

@ -4,6 +4,8 @@ import * as status from "http-status";
import {constants} from "http2"; import {constants} from "http2";
import {Server} from "socket.io"; import {Server} from "socket.io";
import dataaccess from "../lib/dataaccess"; import dataaccess from "../lib/dataaccess";
import {Post} from "../lib/dataaccess/Post";
import {Profile} from "../lib/dataaccess/Profile";
import Route from "../lib/Route"; import Route from "../lib/Route";
/** /**
@ -42,6 +44,14 @@ class HomeRoute extends Route {
*/ */
public resolver(req: any, res: any): any { public resolver(req: any, res: any): any {
return { return {
getSelf() {
if (req.session.userId) {
return new Profile(req.session.userId);
} else {
res.status(status.UNAUTHORIZED);
return new GraphQLError("Not logged in");
}
},
acceptCookies() { acceptCookies() {
req.session.cookiesAccepted = true; req.session.cookiesAccepted = true;
return true; return true;
@ -70,6 +80,34 @@ class HomeRoute extends Route {
return new GraphQLError("User is not logged in."); 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.");
}
},
}; };
} }

@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS posts (
created_at TIMESTAMP DEFAULT now(), created_at TIMESTAMP DEFAULT now(),
content text, content text,
author SERIAL REFERENCES users (id) ON DELETE CASCADE, 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 ( CREATE TABLE IF NOT EXISTS votes (

@ -1,3 +1,8 @@
ALTER TABLE IF EXISTS votes ALTER TABLE IF EXISTS votes
ADD COLUMN IF NOT EXISTS vote_type varchar(8) DEFAULT 'upvote', ADD COLUMN IF NOT EXISTS vote_type varchar(8) DEFAULT 'UPVOTE',
ALTER COLUMN vote_type SET 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;

Loading…
Cancel
Save