API implementation

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

41
package-lock.json generated

@ -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
}
}
},

@ -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 {

@ -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();
}
}
}

@ -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<number> {
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<number> {
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<string> {
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<string> {
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<User> {
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<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.
*/
@ -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;

@ -13,7 +13,7 @@ export class User extends DataObject {
* The name of the user
*/
public async name(): Promise<string> {
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<string> {
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<string> {
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<number> {
this.loadDataIfNotExists();
await this.loadDataIfNotExists();
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
*/
public async joinedAt(): Promise<Date> {
this.loadDataIfNotExists();
await this.loadDataIfNotExists();
return new Date(this.$joinedAt);
}
/**
* 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({
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],
});
}

@ -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.
*/

@ -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]

@ -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.");
}
},
};
}

@ -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 (

@ -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;

Loading…
Cancel
Save