Merge branch 'julius-dev' of Software_Engineering_I/greenvironment-server into develop

pull/2/head
Trivernis 5 years ago committed by Gitea
commit b2501c168b

@ -8,10 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Connection to Postgres Database
- Graphql Schema - Graphql Schema
- default-config file and generation of config file on startup - default-config file and generation of config file on startup
- DTOs - DTOs
- Home Route - Home Route
- database caching
- session management - session management
- Sequelize modules and integration

624
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -35,6 +35,7 @@
"@types/markdown-it": "0.0.9", "@types/markdown-it": "0.0.9",
"@types/node": "^12.7.8", "@types/node": "^12.7.8",
"@types/pg": "^7.11.0", "@types/pg": "^7.11.0",
"@types/sequelize": "^4.28.5",
"@types/socket.io": "^2.1.2", "@types/socket.io": "^2.1.2",
"@types/winston": "^2.4.4", "@types/winston": "^2.4.4",
"delete": "^1.1.0", "delete": "^1.1.0",
@ -49,7 +50,7 @@
}, },
"dependencies": { "dependencies": {
"compression": "^1.7.4", "compression": "^1.7.4",
"connect-pg-simple": "^6.0.1", "connect-session-sequelize": "^6.0.0",
"cookie-parser": "^1.4.4", "cookie-parser": "^1.4.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
@ -57,7 +58,6 @@
"express-session": "^1.16.2", "express-session": "^1.16.2",
"express-socket.io-session": "^1.3.5", "express-socket.io-session": "^1.3.5",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"g": "^2.0.1",
"graphql": "^14.4.2", "graphql": "^14.4.2",
"graphql-import": "^0.7.1", "graphql-import": "^0.7.1",
"http-status": "^1.3.2", "http-status": "^1.3.2",
@ -66,7 +66,9 @@
"markdown-it-emoji": "^1.4.0", "markdown-it-emoji": "^1.4.0",
"pg": "^7.12.1", "pg": "^7.12.1",
"pug": "^2.0.4", "pug": "^2.0.4",
"sequelize": "^5.19.6",
"socket.io": "^2.2.0", "socket.io": "^2.2.0",
"sqlite3": "^4.1.0",
"winston": "^3.2.1" "winston": "^3.2.1"
} }
} }

@ -1,5 +1,4 @@
import * as compression from "compression"; import * as compression from "compression";
import connectPgSimple = require("connect-pg-simple");
import * as cookieParser from "cookie-parser"; import * as cookieParser from "cookie-parser";
import * as cors from "cors"; import * as cors from "cors";
import * as express from "express"; import * as express from "express";
@ -10,33 +9,35 @@ import {buildSchema} from "graphql";
import {importSchema} from "graphql-import"; import {importSchema} from "graphql-import";
import * as http from "http"; import * as http from "http";
import * as path from "path"; import * as path from "path";
import {Sequelize} from "sequelize";
import * as socketIo from "socket.io"; import * as socketIo from "socket.io";
import {resolver} from "./graphql/resolvers"; import {resolver} from "./graphql/resolvers";
import dataaccess, {queryHelper} from "./lib/dataaccess"; import dataaccess from "./lib/dataaccess";
import globals from "./lib/globals"; import globals from "./lib/globals";
import routes from "./routes"; import routes from "./routes";
import * as fsx from "fs-extra";
const SequelizeStore = require("connect-session-sequelize")(session.Store);
const logger = globals.logger; const logger = globals.logger;
const PgSession = connectPgSimple(session);
class App { class App {
public app: express.Application; public app: express.Application;
public io: socketIo.Server; public io: socketIo.Server;
public server: http.Server; public server: http.Server;
public readonly sequelize: Sequelize;
constructor() { constructor() {
this.app = express(); this.app = express();
this.server = new http.Server(this.app); this.server = new http.Server(this.app);
this.io = socketIo(this.server); this.io = socketIo(this.server);
this.sequelize = new Sequelize(globals.config.database.connectionUri);
} }
/** /**
* initializes everything that needs to be initialized asynchronous. * initializes everything that needs to be initialized asynchronous.
*/ */
public async init() { public async init() {
await dataaccess.init(); await dataaccess.init(this.sequelize);
await routes.ioListeners(this.io);
const appSession = session({ const appSession = session({
cookie: { cookie: {
@ -46,12 +47,14 @@ class App {
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
secret: globals.config.session.secret, secret: globals.config.session.secret,
store: new PgSession({ store: new SequelizeStore({db: this.sequelize}),
pool: dataaccess.pool,
tableName: "user_sessions",
}),
}); });
const force = fsx.existsSync("sqz-force");
logger.info(`Sequelize Table force: ${force}`);
await this.sequelize.sync({force, logging: (msg) => logger.silly(msg)});
await routes.ioListeners(this.io);
this.io.use(sharedsession(appSession, {autoSave: true})); this.io.use(sharedsession(appSession, {autoSave: true}));
this.app.set("views", path.join(__dirname, "views")); this.app.set("views", path.join(__dirname, "views"));

@ -1,10 +1,6 @@
# database connection info # database connection info
database: database:
host: connectionUri: "sqlite://:memory:"
port:
user:
password:
database:
# http server configuration # http server configuration
server: server:

@ -1,7 +1,9 @@
import {GraphQLError} from "graphql"; import {GraphQLError} from "graphql";
import * as status from "http-status"; import * as status from "http-status";
import {Sequelize} from "sequelize";
import dataaccess from "../lib/dataaccess"; import dataaccess from "../lib/dataaccess";
import {Chatroom} from "../lib/dataaccess/Chatroom"; import {Chatroom} from "../lib/dataaccess/Chatroom";
import * as models from "../lib/dataaccess/datamodels";
import {Post} from "../lib/dataaccess/Post"; import {Post} from "../lib/dataaccess/Post";
import {Profile} from "../lib/dataaccess/Profile"; import {Profile} from "../lib/dataaccess/Profile";
import {User} from "../lib/dataaccess/User"; import {User} from "../lib/dataaccess/User";
@ -17,9 +19,10 @@ import {is} from "../lib/regex";
*/ */
export function resolver(req: any, res: any): any { export function resolver(req: any, res: any): any {
return { return {
getSelf() { async getSelf() {
if (req.session.userId) { if (req.session.userId) {
return new Profile(req.session.userId); const user = await models.SqUser.findByPk(req.session.userId);
return user.profile;
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
@ -29,7 +32,8 @@ export function resolver(req: any, res: any): any {
if (handle) { if (handle) {
return await dataaccess.getUserByHandle(handle); return await dataaccess.getUserByHandle(handle);
} else if (userId) { } else if (userId) {
return new User(userId); const user = await models.SqUser.findByPk(userId);
return user.user;
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
return new GraphQLError("No userId or handle provided."); return new GraphQLError("No userId or handle provided.");
@ -45,7 +49,8 @@ export function resolver(req: any, res: any): any {
}, },
async getChat({chatId}: { chatId: number }) { async getChat({chatId}: { chatId: number }) {
if (chatId) { if (chatId) {
return new Chatroom(chatId); const chat = await models.SqChat.findByPk(chatId);
return new Chatroom(chat);
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
return new GraphQLError("No chatId given."); return new GraphQLError("No chatId given.");
@ -105,7 +110,8 @@ export function resolver(req: any, res: any): any {
async vote({postId, type}: { postId: number, type: dataaccess.VoteType }) { async vote({postId, type}: { postId: number, type: dataaccess.VoteType }) {
if (postId && type) { if (postId && type) {
if (req.session.userId) { if (req.session.userId) {
return await (new Post(postId)).vote(req.session.userId, type); const post = await models.SqPost.findByPk(postId);
return await post.post.vote(req.session.userId, type);
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
@ -132,7 +138,7 @@ export function resolver(req: any, res: any): any {
}, },
async deletePost({postId}: { postId: number }) { async deletePost({postId}: { postId: number }) {
if (postId) { if (postId) {
const post = new Post(postId); const post = (await models.SqPost.findByPk(postId)).post;
if ((await post.author()).id === req.session.userId) { if ((await post.author()).id === req.session.userId) {
return await dataaccess.deletePost(post.id); return await dataaccess.deletePost(post.id);
} else { } else {

@ -206,6 +206,9 @@ type ChatRoom {
} }
type ChatMessage { type ChatMessage {
"Id of the chat message"
id: ID!
"The author of the chat message." "The author of the chat message."
author: User! author: User!

@ -1,212 +0,0 @@
/**
* @author Trivernis
* @remarks
*
* Taken from {@link https://github.com/Trivernis/whooshy}
*/
import * as fsx from "fs-extra";
import {Pool, PoolClient, QueryConfig, QueryResult} from "pg";
import globals from "./globals";
const logger = globals.logger;
export interface IAdvancedQueryConfig extends QueryConfig {
cache?: boolean;
}
/**
* Transaction class to wrap SQL transactions.
*/
export class SqlTransaction {
/**
* Constructor.
* @param client
*/
constructor(private client: PoolClient) {
}
/**
* Begins the transaction.
*/
public async begin() {
return await this.client.query("BEGIN");
}
/**
* Commits the transaction
*/
public async commit() {
return await this.client.query("COMMIT");
}
/**
* Rolls back the transaction
*/
public async rollback() {
return await this.client.query("ROLLBACK");
}
/**
* Executes a query inside the transaction.
* @param query
*/
public async query(query: QueryConfig) {
return await this.client.query(query);
}
/**
* Releases the client back to the pool.
*/
public release() {
this.client.release();
}
}
/**
* Query helper for easyer fetching of a specific row count.
*/
export class QueryHelper {
private pool: Pool;
/**
* Constructor.
* @param pgPool
* @param [tableCreationFile]
* @param [tableUpdateFile]
*/
constructor(pgPool: Pool, private tableCreationFile?: string, private tableUpdateFile?: string) {
this.pool = pgPool;
}
/**
* Async init function
*/
public async init() {
await this.pool.connect();
await this.createTables();
await this.updateTableDefinitions();
}
/**
* creates all tables needed if a filepath was given with the constructor
*/
public async createTables() {
if (this.tableCreationFile) {
logger.info("Creating nonexistent tables...");
const tableSql = await fsx.readFile(this.tableCreationFile, "utf-8");
const trans = await this.createTransaction();
await trans.begin();
try {
await trans.query({text: tableSql});
await trans.commit();
} catch (err) {
globals.logger.error(`Error on table creation ${err.message}`);
globals.logger.debug(err.stack);
await trans.rollback();
} finally {
trans.release();
}
}
}
/**
* Updates the definition of the tables if the table update file was passed in the constructor
*/
public async updateTableDefinitions() {
if (this.tableUpdateFile) {
logger.info("Updating table definitions...");
const tableSql = await fsx.readFile(this.tableUpdateFile, "utf-8");
const trans = await this.createTransaction();
await trans.begin();
try {
await trans.query({text: tableSql});
await trans.commit();
} catch (err) {
globals.logger.error(`Error on table update ${err.message}`);
globals.logger.debug(err.stack);
await trans.rollback();
} finally {
trans.release();
}
}
}
/**
* executes the sql query with values and returns all results.
* @param query
*/
public async all(query: IAdvancedQueryConfig): Promise<any[]> {
const result = await this.query(query);
return result.rows;
}
/**
* executes the sql query with values and returns the first result.
* @param query
*/
public async first(query: IAdvancedQueryConfig): Promise<any> {
const result = await this.query(query);
if (result.rows && result.rows.length > 0) {
return result.rows[0];
}
}
/**
* Creates a new Transaction to be uses with error handling.
*/
public async createTransaction() {
const client: PoolClient = await this.pool.connect();
return new SqlTransaction(client);
}
/**
* Queries the database with error handling.
* @param query - the sql and values to execute
*/
private async query(query: IAdvancedQueryConfig): Promise<QueryResult|{rows: any}> {
try {
query.text = query.text.replace(/[\r\n]/g, " ");
globals.logger.silly(`Executing sql '${JSON.stringify(query)}'`);
if (query.cache) {
const key = globals.cache.hashKey(JSON.stringify(query));
const cacheResult = globals.cache.get(key);
if (cacheResult) {
return cacheResult;
} else {
const result = await this.pool.query(query);
globals.cache.set(key, result);
return result;
}
} else {
return await this.pool.query(query);
}
} catch (err) {
logger.debug(`Error on query "${JSON.stringify(query)}".`);
logger.error(`Sql query failed: ${err}`);
logger.verbose(err.stack);
return {
rows: null,
};
}
}
}
/**
* Returns the parameterized value sql for inserting
* @param columnCount
* @param rowCount
* @param [offset]
*/
export function buildSqlParameters(columnCount: number, rowCount: number, offset?: number): string {
let sql = "";
for (let i = 0; i < rowCount; i++) {
sql += "(";
for (let j = 0; j < columnCount; j++) {
sql += `$${(i * columnCount) + j + 1 + offset},`;
}
sql = sql.replace(/,$/, "") + "),";
}
return sql.replace(/,$/, "");
}

@ -20,6 +20,7 @@ abstract class Route {
protected ions?: Namespace; protected ions?: Namespace;
public abstract async init(...params: any): Promise<any>; public abstract async init(...params: any): Promise<any>;
public abstract async destroy(...params: any): Promise<any>; public abstract async destroy(...params: any): Promise<any>;
} }

@ -1,31 +1,38 @@
import markdown from "../markdown"; import markdown from "../markdown";
import {Chatroom} from "./Chatroom"; import {Chatroom} from "./Chatroom";
import * as models from "./datamodels/models";
import {User} from "./User"; import {User} from "./User";
export class ChatMessage { export class ChatMessage {
constructor(
public readonly author: User, public id: number;
public readonly chat: Chatroom, public content: string;
public readonly createdAt: number, public createdAt: Date;
public readonly content: string) {}
constructor(private message: models.ChatMessage) {
this.id = message.id;
this.content = message.content;
this.createdAt = message.createdAt;
}
/**
* returns the author of the chat message.
*/
public async author(): Promise<User> {
return new User(await this.message.getAuthor());
}
/** /**
* The content rendered by markdown-it. * Returns the rendered html content of the chat message.
*/ */
public htmlContent(): string { public htmlContent(): string {
return markdown.renderInline(this.content); return markdown.renderInline(this.content);
} }
/** /**
* Returns resolved and rendered content of the chat message. * returns the chatroom for the chatmessage.
*/ */
public resolvedContent() { public async chat(): Promise<Chatroom> {
return { return (await this.message.getChat()).chatroom;
author: this.author.id,
chat: this.chat.id,
content: this.content,
createdAt: this.createdAt,
htmlContent: this.htmlContent(),
};
} }
} }

@ -1,44 +1,22 @@
import globals from "../globals"; import {SqChat} from "./datamodels";
import {ChatMessage} from "./ChatMessage";
import {queryHelper} from "./index";
import {User} from "./User"; import {User} from "./User";
export class Chatroom { export class Chatroom {
public readonly id: number;
public namespace: string; public namespace: string;
constructor(public readonly id: number) {
this.id = Number(id);
this.namespace = `/chat/${id}`;
}
/** constructor(private chat: SqChat) {
* Returns if the chat exists. this.id = chat.id;
*/ this.namespace = `/chat/${chat.id}`;
public async exists(): Promise<boolean> {
const result = await queryHelper.first({
text: "SELECT id FROM chats WHERE id = $1",
values: [this.id],
});
return !!result.id;
} }
/** /**
* Returns all members of a chatroom. * Returns all members of a chatroom.
*/ */
public async members(): Promise<User[]> { public async members(): Promise<User[]> {
const result = await queryHelper.all({ const members = await this.chat.getMembers();
cache: true, return members.map((m) => new User(m));
text: `SELECT * FROM chat_members
JOIN users ON (chat_members.member = users.id)
WHERE chat_members.chat = $1;`,
values: [this.id],
});
const chatMembers = [];
for (const row of result) {
const user = new User(row.id, row);
chatMembers.push(user);
}
return chatMembers;
} }
/** /**
@ -47,30 +25,14 @@ export class Chatroom {
* @param offset - the offset of messages to return * @param offset - the offset of messages to return
* @param containing - filter by containing * @param containing - filter by containing
*/ */
public async messages({first, offset, containing}: {first?: number, offset?: number, containing?: string}) { public async messages({first, offset, containing}: { first?: number, offset?: number, containing?: string }) {
const lim = first || 16; const lim = first || 16;
const offs = offset || 0; const offs = offset || 0;
const messages = await this.chat.getMessages({limit: lim, offset: offs});
const result = await queryHelper.all({
cache: true,
text: "SELECT * FROM chat_messages WHERE chat = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
values: [this.id, lim, offs],
});
const messages = [];
const users: any = {};
for (const row of result) {
if (!users[row.author]) {
const user = new User(row.author);
await user.exists();
users[row.author] = user;
}
messages.push(new ChatMessage(users[row.author], this, row.created_at, row.content));
}
if (containing) { if (containing) {
return messages.filter((x) => x.content.includes(containing)); return messages.filter((x) => x.content.includes(containing)).map((m) => m.message);
} else { } else {
return messages; return messages.map((m) => m.message);
} }
} }
} }

@ -1,40 +0,0 @@
/**
* abstact DataObject class
*/
import {EventEmitter} from "events";
export abstract class DataObject extends EventEmitter {
protected dataLoaded: boolean = false;
private loadingData: boolean = false;
constructor(public id: number, protected row?: any) {
super();
this.id = Number(id);
}
/**
* Returns if the object extists by trying to load data.
*/
public async exists() {
await this.loadDataIfNotExists();
return this.dataLoaded;
}
protected abstract loadData(): Promise<void>;
/**
* Loads data from the database if data has not been loaded
*/
protected async loadDataIfNotExists() {
if (!this.dataLoaded && !this.loadingData) {
this.loadingData = true;
await this.loadData();
this.loadingData = false;
this.emit("loaded");
} else if (this.loadingData) {
return new Promise((res) => {
this.on("loaded", () => res());
});
}
}
}

@ -1,106 +1,68 @@
import markdown from "../markdown"; import markdown from "../markdown";
import {DataObject} from "./DataObject"; import {SqPost, SqPostVotes} from "./datamodels";
import {queryHelper} from "./index"; import {PostVotes} from "./datamodels/models";
import dataaccess from "./index"; import dataaccess from "./index";
import {User} from "./User"; import {User} from "./User";
export class Post extends DataObject { export class Post {
public readonly id: number; public readonly id: number;
private $createdAt: string; public createdAt: Date;
private $content: string; public content: string;
private $author: number; public type: string;
private $type: string;
/** private post: SqPost;
* Returns the resolved data of the post.
*/ constructor(post: SqPost) {
public async resolvedData() { this.id = post.id;
await this.loadDataIfNotExists(); this.createdAt = post.createdAt;
return { this.post = post;
authorId: this.$author, this.type = "";
content: this.$content, this.content = post.content;
createdAt: this.$createdAt,
id: this.id,
type: this.$type,
};
} }
/** /**
* Returns the upvotes of a post. * Returns the upvotes of a post.
*/ */
public async upvotes(): Promise<number> { public async upvotes(): Promise<number> {
const result = await queryHelper.first({ return PostVotes.count({where: {voteType: dataaccess.VoteType.UPVOTE, post_id: this.id}});
cache: true,
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> {
const result = await queryHelper.first({ return PostVotes.count({where: {voteType: dataaccess.VoteType.DOWNVOTE, post_id: this.id}});
cache: true,
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> {
await this.loadDataIfNotExists();
return this.$content;
} }
/** /**
* the content rendered by markdown-it. * the content rendered by markdown-it.
*/ */
public async htmlContent(): Promise<string> { public async htmlContent(): Promise<string> {
await this.loadDataIfNotExists(); return markdown.render(this.content);
return markdown.render(this.$content);
}
/**
* The date the post was created at.
*/
public async createdAt(): Promise<string> {
await this.loadDataIfNotExists();
return this.$createdAt;
} }
/** /**
* The autor of the post. * The autor of the post.
*/ */
public async author(): Promise<User> { public async author(): Promise<User> {
await this.loadDataIfNotExists(); return new User(await this.post.getUser());
return new User(this.$author);
} }
/** /**
* Deletes the post. * Deletes the post.
*/ */
public async delete(): Promise<void> { public async delete(): Promise<void> {
const query = await queryHelper.first({ await this.post.destroy();
text: "DELETE FROM posts WHERE id = $1",
values: [this.id],
});
} }
/** /**
* The type of vote the user performed on the post. * The type of vote the user performed on the post.
*/ */
public async userVote(userId: number): Promise<dataaccess.VoteType> { public async userVote(userId: number): Promise<dataaccess.VoteType> {
const result = await queryHelper.first({ const votes = await this.post.getVotes({where: {userId}});
cache: true,
text: "SELECT vote_type FROM votes WHERE user_id = $1 AND item_id = $2", if (votes.length >= 1) {
values: [userId, this.id], return votes[0].voteType;
});
if (result) {
return result.vote_type;
} else { } else {
return null; return null;
} }
@ -112,48 +74,22 @@ export class Post extends DataObject {
* @param type * @param type
*/ */
public async vote(userId: number, type: dataaccess.VoteType): Promise<dataaccess.VoteType> { public async vote(userId: number, type: dataaccess.VoteType): Promise<dataaccess.VoteType> {
const uVote = await this.userVote(userId); type = type || dataaccess.VoteType.UPVOTE;
if (uVote === type) { let vote = await SqPostVotes.findOne({where: {user_id: userId, post_id: this.id}});
await queryHelper.first({ if (!vote) {
text: "DELETE FROM votes WHERE item_id = $1 AND user_id = $2", await this.post.addVote(userId);
values: [this.id, userId], vote = await SqPostVotes.findOne({where: {user_id: userId, post_id: this.id}});
}); }
} else { if (vote) {
if (uVote) { if (vote.voteType === type) {
await queryHelper.first({ await vote.destroy();
text: "UPDATE votes SET vote_type = $1 WHERE user_id = $2 AND item_id = $3", return null;
values: [type, userId, this.id],
});
} else { } else {
await queryHelper.first({ vote.voteType = type;
text: "INSERT INTO votes (user_id, item_id, vote_type) values ($1, $2, $3)", await vote.save();
values: [userId, this.id, type],
});
} }
return type;
} }
}
/** return vote.voteType;
* Loads the data from the database if needed.
*/
protected async loadData(): Promise<void> {
let result: any;
if (this.row) {
result = this.row;
} else {
result = await queryHelper.first({
cache: true,
text: "SELECT * FROM posts WHERE posts.id = $1",
values: [this.id],
});
}
if (result) {
this.$author = result.author;
this.$content = result.content;
this.$createdAt = result.created_at;
this.$type = result.type;
this.dataLoaded = true;
}
} }
} }

@ -1,10 +1,61 @@
import {RequestNotFoundError} from "../errors/RequestNotFoundError"; import {RequestNotFoundError} from "../errors/RequestNotFoundError";
import {Chatroom} from "./Chatroom"; import {Chatroom} from "./Chatroom";
import dataaccess, {queryHelper} from "./index"; import {SqUser} from "./datamodels";
import {User} from "./User"; import dataaccess from "./index";
import {Request} from "./Request"; import * as wrappers from "./wrappers";
export class Profile extends User { export class Profile {
public id: number;
public name: string;
public handle: string;
public email: string;
public greenpoints: number;
public joinedAt: Date;
protected user: SqUser;
constructor(user: SqUser) {
this.name = user.username;
this.handle = user.handle;
this.email = user.email;
this.greenpoints = user.rankpoints;
this.joinedAt = user.joinedAt;
this.id = user.id;
this.user = user;
}
/**
* Returns the number of posts the user created
*/
public async numberOfPosts(): Promise<number> {
return this.user.countPosts();
}
/**
* Returns all friends of the user.
*/
public async friends(): Promise<wrappers.User[]> {
const result = await this.user.getFriends();
const userFriends = [];
for (const friend of result) {
userFriends.push(new wrappers.User(friend));
}
return userFriends;
}
/**
* Returns all posts for a user.
*/
public async posts({first, offset}: { first: number, offset: number }): Promise<wrappers.Post[]> {
const postRes = await this.user.getPosts();
const posts = [];
for (const post of postRes) {
posts.push(new wrappers.Post(post));
}
return posts;
}
/** /**
* Returns all chatrooms (with pagination). * Returns all chatrooms (with pagination).
@ -12,19 +63,14 @@ export class Profile extends User {
* @param first * @param first
* @param offset * @param offset
*/ */
public async chats({first, offset}: {first: number, offset?: number}): Promise<Chatroom[]> { public async chats({first, offset}: { first: number, offset?: number }): Promise<Chatroom[]> {
if (!(await this.exists())) {
return [];
}
first = first || 10; first = first || 10;
offset = offset || 0; offset = offset || 0;
const result = await queryHelper.all({ const result = await this.user.getChats();
text: "SELECT chat FROM chat_members WHERE member = $1 LIMIT $2 OFFSET $3",
values: [this.id, first, offset],
});
if (result) { if (result) {
return result.map((row) => new Chatroom(row.chat)); return result.map((chat) => new Chatroom(chat));
} else { } else {
return []; return [];
} }
@ -34,24 +80,14 @@ export class Profile extends User {
* Returns all open requests the user has send. * Returns all open requests the user has send.
*/ */
public async sentRequests() { public async sentRequests() {
const result = await queryHelper.all({ return this.user.getSentRequests();
cache: true,
text: "SELECT * FROM requests WHERE sender = $1",
values: [this.id],
});
return this.getRequests(result);
} }
/** /**
* Returns all received requests of the user. * Returns all received requests of the user.
*/ */
public async receivedRequests() { public async receivedRequests() {
const result = await queryHelper.all({ return this.user.getReceivedRequests();
cache: true,
text: "SELECT * FROM requests WHERE receiver = $1",
values: [this.id],
});
return this.getRequests(result);
} }
/** /**
@ -59,11 +95,9 @@ export class Profile extends User {
* @param points * @param points
*/ */
public async setGreenpoints(points: number): Promise<number> { public async setGreenpoints(points: number): Promise<number> {
const result = await queryHelper.first({ this.user.rankpoints = points;
text: "UPDATE users SET greenpoints = $1 WHERE id = $2 RETURNING greenpoints", await this.user.save();
values: [points, this.id], return this.user.rankpoints;
});
return result.greenpoints;
} }
/** /**
@ -71,22 +105,18 @@ export class Profile extends User {
* @param email * @param email
*/ */
public async setEmail(email: string): Promise<string> { public async setEmail(email: string): Promise<string> {
const result = await queryHelper.first({ this.user.email = email;
text: "UPDATE users SET email = $1 WHERE users.id = $2 RETURNING email", await this.user.save();
values: [email, this.id], return this.user.email;
});
return result.email;
} }
/** /**
* Updates the handle of the user * Updates the handle of the user
*/ */
public async setHandle(handle: string): Promise<string> { public async setHandle(handle: string): Promise<string> {
const result = await queryHelper.first({ this.user.handle = handle;
text: "UPDATE users SET handle = $1 WHERE id = $2", await this.user.save();
values: [handle, this.id], return this.user.handle;
});
return result.handle;
} }
/** /**
@ -94,11 +124,9 @@ export class Profile extends User {
* @param name * @param name
*/ */
public async setName(name: string): Promise<string> { public async setName(name: string): Promise<string> {
const result = await queryHelper.first({ this.user.username = name;
text: "UPDATE users SET name = $1 WHERE id = $2", await this.user.save();
values: [name, this.id], return this.user.username;
});
return result.name;
} }
/** /**
@ -107,10 +135,10 @@ export class Profile extends User {
* @param type * @param type
*/ */
public async denyRequest(sender: number, type: dataaccess.RequestType) { public async denyRequest(sender: number, type: dataaccess.RequestType) {
await queryHelper.first({ const request = await this.user.getReceivedRequests({where: {senderId: sender, requestType: type}});
text: "DELETE FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3", if (request[0]) {
values: [this.id, sender, type], await request[0].destroy();
}); }
} }
/** /**
@ -119,45 +147,15 @@ export class Profile extends User {
* @param type * @param type
*/ */
public async acceptRequest(sender: number, type: dataaccess.RequestType) { public async acceptRequest(sender: number, type: dataaccess.RequestType) {
const exists = await queryHelper.first({ const requests = await this.user.getReceivedRequests({where: {senderId: sender, requestType: type}});
cache: true, if (requests.length > 0) {
text: "SELECT 1 FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3", const request = requests[0];
values: [this.id, sender, type], if (request.requestType === dataaccess.RequestType.FRIENDREQUEST) {
}); await this.user.addFriend(sender);
if (exists) { await request.destroy();
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 { } else {
throw new RequestNotFoundError(sender, this.id, type); 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;
}
} }

@ -1,24 +0,0 @@
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) {
}
/**
* Returns the resolved request data.
*/
public resolvedData() {
return {
receiverId: this.receiver.id,
senderId: this.sender.id,
type: this.type,
};
}
}

@ -1,84 +1,39 @@
import globals from "../globals"; import {SqUser} from "./datamodels";
import {DataObject} from "./DataObject"; import * as wrappers from "./wrappers";
import {queryHelper} from "./index";
import {Post} from "./Post";
export class User extends DataObject { export class User {
private $name: string; public id: number;
private $handle: string; public name: string;
private $email: string; public handle: string;
private $greenpoints: number; public greenpoints: number;
private $joinedAt: string; public joinedAt: Date;
private $exists: boolean;
/** protected user: SqUser;
* The name of the user
*/
public async name(): Promise<string> {
await this.loadDataIfNotExists();
return this.$name;
}
/**
* The unique handle of the user.
*/
public async handle(): Promise<string> {
await this.loadDataIfNotExists();
return this.$handle;
}
/** constructor(user: SqUser) {
* The email of the user this.id = user.id;
*/ this.name = user.username;
public async email(): Promise<string> { this.handle = user.handle;
await this.loadDataIfNotExists(); this.greenpoints = user.rankpoints;
return this.$email; this.joinedAt = user.joinedAt;
} this.user = user;
/**
* The number of greenpoints of the user
*/
public async greenpoints(): Promise<number> {
await this.loadDataIfNotExists();
return this.$greenpoints;
} }
/** /**
* Returns the number of posts the user created * Returns the number of posts the user created
*/ */
public async numberOfPosts(): Promise<number> { public async numberOfPosts(): Promise<number> {
const result = await queryHelper.first({ return this.user.countPosts();
cache: true,
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> {
await this.loadDataIfNotExists();
return new Date(this.$joinedAt);
} }
/** /**
* Returns all friends of the user. * Returns all friends of the user.
*/ */
public async friends(): Promise<User[]> { public async friends(): Promise<User[]> {
const result = await queryHelper.all({ const result = await this.user.getFriends();
cache: true,
text: "SELECT * FROM user_friends WHERE user_id = $1 OR friend_id = $1",
values: [this.id],
});
const userFriends = []; const userFriends = [];
for (const row of result) { for (const friend of result) {
if (row.user_id === this.id) { userFriends.push(new User(friend));
userFriends.push(new User(row.friend_id));
} else {
userFriends.push(new User(row.user_id));
}
} }
return userFriends; return userFriends;
} }
@ -86,43 +41,13 @@ export class User extends DataObject {
/** /**
* Returns all posts for a user. * Returns all posts for a user.
*/ */
public async posts({first, offset}: {first: number, offset: number}): Promise<Post[]> { public async posts({first, offset}: { first: number, offset: number }): Promise<wrappers.Post[]> {
first = first || 10; const postRes = await this.user.getPosts();
offset = offset || 0;
const result = await queryHelper.all({
cache: true,
text: "SELECT * FROM posts WHERE author = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
values: [this.id, first, offset],
});
const posts = []; const posts = [];
for (const row of result) { for (const post of postRes) {
posts.push(new Post(row.id, row)); posts.push(new wrappers.Post(post));
} }
return posts; return posts;
} }
/**
* Fetches the data for the user.
*/
protected async loadData(): Promise<void> {
let result: any;
if (this.row) {
result = this.row;
} else {
result = await queryHelper.first({
cache: true,
text: "SELECT * FROM users WHERE users.id = $1",
values: [this.id],
});
}
if (result) {
this.$name = result.name;
this.$handle = result.handle;
this.$email = result.email;
this.$greenpoints = result.greenpoints;
this.$joinedAt = result.joined_at;
this.dataLoaded = true;
}
}
} }

@ -0,0 +1,12 @@
export {
init as datainit,
User as SqUser,
Post as SqPost,
Chat as SqChat,
Request as SqRequest,
PostVotes as SqPostVotes,
ChatMessage as SqChatMessage,
ChatMembers as SqChatMembers,
RequestType as SqRequestType,
UserFriends as SqUserFriends,
} from "./models";

@ -0,0 +1,279 @@
// tslint:disable:object-literal-sort-keys
import * as sqz from "sequelize";
import {
Association,
BelongsToGetAssociationMixin,
BelongsToManyAddAssociationMixin,
BelongsToManyCountAssociationsMixin,
BelongsToManyCreateAssociationMixin,
BelongsToManyGetAssociationsMixin,
BelongsToManyHasAssociationMixin,
DataTypes,
HasManyAddAssociationMixin,
HasManyCountAssociationsMixin,
HasManyCreateAssociationMixin,
HasManyGetAssociationsMixin,
HasManyHasAssociationMixin,
HasOneGetAssociationMixin,
Model,
Sequelize,
} from "sequelize";
import * as wrappers from "../wrappers";
const underscored = true;
enum VoteType {
UPVOTE = "UPVOTE",
DOWNVOTE = "DOWNVOTE",
}
export enum RequestType {
FRIENDREQUEST = "FRIENDREQUEST",
GROUPINVITE = "GROUPINVITE",
EVENTINVITE = "EVENTINVITE",
}
export class User extends Model {
public static associations: {
friends: Association<User, User>;
posts: Association<User, Post>;
votes: Association<User, PostVotes>;
requests: Association<User, Request>;
};
public id!: number;
public username!: string;
public handle!: string;
public email!: string;
public password!: string;
public rankpoints!: number;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
public getFriends!: HasManyGetAssociationsMixin<User>;
public addFriend!: HasManyAddAssociationMixin<User, number>;
public hasFriend!: HasManyHasAssociationMixin<User, number>;
public countFriends!: HasManyCountAssociationsMixin;
public getPosts!: HasManyGetAssociationsMixin<Post>;
public addPost!: HasManyAddAssociationMixin<Post, number>;
public hasPost!: HasManyHasAssociationMixin<Post, number>;
public countPosts!: HasManyCountAssociationsMixin;
public createPost!: HasManyCreateAssociationMixin<Post>;
public getReceivedRequests!: HasManyGetAssociationsMixin<Request>;
public addReceivedRequest!: HasManyAddAssociationMixin<Request, number>;
public hasReceivedRequest!: HasManyHasAssociationMixin<Request, number>;
public countReceivedRequests!: HasManyCountAssociationsMixin;
public createReceivedRequest!: HasManyCreateAssociationMixin<Request>;
public getSentRequests!: HasManyGetAssociationsMixin<Request>;
public addSentRequest!: HasManyAddAssociationMixin<Request, number>;
public hasSentRequest!: HasManyHasAssociationMixin<Request, number>;
public countSentRequests!: HasManyCountAssociationsMixin;
public createSentRequest!: HasManyCreateAssociationMixin<Request>;
public getChats!: BelongsToManyGetAssociationsMixin<Chat>;
public addChat!: BelongsToManyAddAssociationMixin<Chat, number>;
public hasChat!: BelongsToManyHasAssociationMixin<Chat, number>;
public countChats!: BelongsToManyCountAssociationsMixin;
public createChat!: BelongsToManyCreateAssociationMixin<Chat>;
/**
* Getter for joined at as the date the entry was created.
*/
public get joinedAt(): Date {
// @ts-ignore
return this.getDataValue("createdAt");
}
/**
* Wraps itself into a user
*/
public get user(): wrappers.User {
return new wrappers.User(this);
}
/**
* returns the username.
*/
public get name(): string {
return this.getDataValue("username");
}
/**
* Wraps itself into a profile.
*/
public get profile(): wrappers.Profile {
return new wrappers.Profile(this);
}
}
export class UserFriends extends Model {
}
export class Post extends Model {
public static associations: {
author: Association<Post, User>,
votes: Association<Post, PostVotes>,
};
public id!: number;
public content!: string;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
public getUser!: BelongsToGetAssociationMixin<User>;
public getVotes!: HasManyGetAssociationsMixin<PostVotes>;
public addVote!: HasManyAddAssociationMixin<PostVotes, number>;
public hasVote!: HasManyHasAssociationMixin<PostVotes, number>;
public countVotes!: HasManyCountAssociationsMixin;
public createVote!: HasManyCreateAssociationMixin<PostVotes>;
/**
* Wraps itself into a Post instance.
*/
public get post(): wrappers.Post {
return new wrappers.Post(this);
}
}
export class PostVotes extends Model {
public voteType: VoteType;
}
export class Request extends Model {
public id!: number;
public requestType!: RequestType;
public getSender!: HasOneGetAssociationMixin<User>;
public getReceiver!: HasOneGetAssociationMixin<User>;
}
export class Chat extends Model {
public static associations: {
members: Association<Chat, User>,
messages: Association<Chat, ChatMessage>,
};
public id!: number;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
public getMembers!: BelongsToManyGetAssociationsMixin<User>;
public addMember!: BelongsToManyAddAssociationMixin<User, number>;
public hasMember!: BelongsToManyHasAssociationMixin<User, number>;
public countMembers!: BelongsToManyCountAssociationsMixin;
public getMessages!: HasManyGetAssociationsMixin<ChatMessage>;
public addMessage!: HasManyAddAssociationMixin<ChatMessage, number>;
public hasMessage!: HasManyHasAssociationMixin<ChatMessage, number>;
public countMessages!: HasManyCountAssociationsMixin;
public createMessage!: HasManyCreateAssociationMixin<ChatMessage>;
/**
* wraps itself into a chatroom.
*/
public get chatroom(): wrappers.Chatroom {
return new wrappers.Chatroom(this);
}
}
export class ChatMembers extends Model {
}
export class ChatMessage extends Model {
public id: number;
public content!: string;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
public getAuthor!: BelongsToGetAssociationMixin<User>;
public getChat!: BelongsToGetAssociationMixin<Chat>;
public get message(): wrappers.ChatMessage {
return new wrappers.ChatMessage(this);
}
}
export function init(sequelize: Sequelize) {
User.init({
username: {
allowNull: false,
type: sqz.STRING(128),
},
handle: {
allowNull: false,
type: sqz.STRING(128),
unique: true,
},
email: {
allowNull: false,
type: sqz.STRING(128),
unique: true,
},
password: {
allowNull: false,
type: sqz.STRING(128),
},
rankpoints: {
allowNull: false,
type: DataTypes.INTEGER,
defaultValue: 0,
},
}, {sequelize, underscored});
UserFriends.init({}, {sequelize, underscored});
Post.init({
content: DataTypes.TEXT,
}, {sequelize, underscored});
PostVotes.init({
voteType: {
type: DataTypes.ENUM,
values: ["UPVOTE", "DOWNVOTE"],
},
}, {sequelize, underscored});
Request.init({
requestType: {
type: DataTypes.ENUM,
values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"],
},
}, {sequelize, underscored});
Chat.init({}, {sequelize, underscored});
ChatMembers.init({}, {sequelize, underscored});
ChatMessage.init({
content: {
type: DataTypes.TEXT,
allowNull: false,
},
}, {sequelize, underscored});
User.belongsToMany(User, {through: UserFriends, as: "friends"});
Post.belongsTo(User, {foreignKey: "userId"});
User.hasMany(Post, {as: "posts", foreignKey: "userId"});
Post.belongsToMany(User, {through: PostVotes, as: "votes"});
User.belongsToMany(Post, {through: PostVotes, as: "votes"});
User.hasMany(Request, {as: "sentRequests"});
User.hasMany(Request, {as: "receivedRequests"});
User.belongsToMany(Chat, {through: ChatMembers});
Chat.belongsToMany(User, {through: ChatMembers, as: "members"});
Chat.hasMany(ChatMessage, {as: "messages"});
ChatMessage.belongsTo(Chat);
ChatMessage.belongsTo(User, {as: "author", foreignKey: "userId"});
User.hasMany(ChatMessage, {foreignKey: "userId"});
}

@ -1,30 +1,19 @@
import {Pool} from "pg"; import {Sequelize} from "sequelize";
import {ChatNotFoundError} from "../errors/ChatNotFoundError"; import {ChatNotFoundError} from "../errors/ChatNotFoundError";
import {EmailAlreadyRegisteredError} from "../errors/EmailAlreadyRegisteredError"; import {EmailAlreadyRegisteredError} from "../errors/EmailAlreadyRegisteredError";
import {UserNotFoundError} from "../errors/UserNotFoundError"; import {UserNotFoundError} from "../errors/UserNotFoundError";
import globals from "../globals"; import globals from "../globals";
import {InternalEvents} from "../InternalEvents"; import {InternalEvents} from "../InternalEvents";
import {QueryHelper} from "../QueryHelper";
import {ChatMessage} from "./ChatMessage";
import {Chatroom} from "./Chatroom"; import {Chatroom} from "./Chatroom";
import * as models from "./datamodels";
import {Post} from "./Post"; import {Post} from "./Post";
import {Profile} from "./Profile"; import {Profile} from "./Profile";
import {Request} from "./Request";
import {User} from "./User"; import {User} from "./User";
const config = globals.config; const config = globals.config;
const tableCreationFile = __dirname + "/../../sql/create-tables.sql"; const tableCreationFile = __dirname + "/../../sql/create-tables.sql";
const tableUpdateFile = __dirname + "/../../sql/update-tables.sql"; const tableUpdateFile = __dirname + "/../../sql/update-tables.sql";
const dbClient: Pool = new Pool({
database: config.database.database,
host: config.database.host,
password: config.database.password,
port: config.database.port,
user: config.database.user,
});
export const queryHelper = new QueryHelper(dbClient, tableCreationFile, tableUpdateFile);
/** /**
* Generates a new handle from the username and a base64 string of the current time. * Generates a new handle from the username and a base64 string of the current time.
* @param username * @param username
@ -38,14 +27,15 @@ function generateHandle(username: string) {
*/ */
namespace dataaccess { namespace dataaccess {
export const pool: Pool = dbClient; let sequelize: Sequelize;
/** /**
* Initializes everything that needs to be initialized asynchronous. * Initializes everything that needs to be initialized asynchronous.
*/ */
export async function init() { export async function init(seq: Sequelize) {
sequelize = seq;
try { try {
await queryHelper.init(); await models.datainit(sequelize);
} catch (err) { } catch (err) {
globals.logger.error(err.message); globals.logger.error(err.message);
globals.logger.debug(err.stack); globals.logger.debug(err.stack);
@ -57,12 +47,9 @@ namespace dataaccess {
* @param userHandle * @param userHandle
*/ */
export async function getUserByHandle(userHandle: string): Promise<User> { export async function getUserByHandle(userHandle: string): Promise<User> {
const result = await queryHelper.first({ const user = await models.SqUser.findOne({where: {handle: userHandle}});
text: "SELECT * FROM users WHERE users.handle = $1", if (user) {
values: [userHandle], return new User(user);
});
if (result) {
return new User(result.id, result);
} else { } else {
throw new UserNotFoundError(userHandle); throw new UserNotFoundError(userHandle);
} }
@ -74,12 +61,9 @@ namespace dataaccess {
* @param password * @param password
*/ */
export async function getUserByLogin(email: string, password: string): Promise<Profile> { export async function getUserByLogin(email: string, password: string): Promise<Profile> {
const result = await queryHelper.first({ const user = await models.SqUser.findOne({where: {email, password}});
text: "SELECT * FROM users WHERE email = $1 AND password = $2", if (user) {
values: [email, password], return new Profile(user);
});
if (result) {
return new Profile(result.id, result);
} else { } else {
throw new UserNotFoundError(email); throw new UserNotFoundError(email);
} }
@ -92,16 +76,11 @@ namespace dataaccess {
* @param password * @param password
*/ */
export async function registerUser(username: string, email: string, password: string) { export async function registerUser(username: string, email: string, password: string) {
const existResult = await queryHelper.first({ const existResult = !!(await models.SqUser.findOne({where: {username, email, password}}));
text: "SELECT email FROM users WHERE email = $1;", const handle = generateHandle(username);
values: [email], if (!existResult) {
}); const user = await models.SqUser.create({username, email, password, handle});
if (!existResult || !existResult.email) { return new Profile(user);
const result = await queryHelper.first({
text: "INSERT INTO users (name, handle, password, email) VALUES ($1, $2, $3, $4) RETURNING *",
values: [username, generateHandle(username), password, email],
});
return new Profile(result.id, result);
} else { } else {
throw new EmailAlreadyRegisteredError(email); throw new EmailAlreadyRegisteredError(email);
} }
@ -112,12 +91,9 @@ namespace dataaccess {
* @param postId * @param postId
*/ */
export async function getPost(postId: number): Promise<Post> { export async function getPost(postId: number): Promise<Post> {
const result = await queryHelper.first({ const post = await models.SqPost.findByPk(postId);
text: "SELECT * FROM posts WHERE id = $1", if (post) {
values: [postId], return new Post(post);
});
if (result) {
return new Post(result.id, result);
} else { } else {
return null; return null;
} }
@ -131,33 +107,18 @@ namespace dataaccess {
*/ */
export async function getPosts(first: number, offset: number, sort: SortType) { export async function getPosts(first: number, offset: number, sort: SortType) {
if (sort === SortType.NEW) { if (sort === SortType.NEW) {
const results = await queryHelper.all({ const posts = await models.SqPost.findAll({order: [["createdAt", "DESC"]], limit: first, offset});
cache: true, return posts.map((p) => new Post(p));
text: "SELECT * FROM posts ORDER BY created_at DESC LIMIT $1 OFFSET $2",
values: [first, offset],
});
const posts = [];
for (const row of results) {
posts.push(new Post(row.id, row));
}
return posts;
} else { } else {
const results = await queryHelper.all({ const results: models.SqPost[] = await sequelize.query(
cache: true, `SELECT id FROM (
text: ` SELECT *,
SELECT * FROM ( (SELECT count(*) FROM votes WHERE vote_type = 'UPVOTE' AND item_id = posts.id) AS upvotes ,
SELECT *, (SELECT count(*) FROM votes WHERE vote_type = 'DOWNVOTE' AND item_id = posts.id) AS downvotes
(SELECT count(*) FROM votes WHERE vote_type = 'UPVOTE' AND item_id = posts.id) AS upvotes , FROM posts) AS a ORDER BY (a.upvotes - a.downvotes) DESC LIMIT ? OFFSET ?`,
(SELECT count(*) FROM votes WHERE vote_type = 'DOWNVOTE' AND item_id = posts.id) AS downvotes {replacements: [first, offset], mapToModel: true, model: models.SqPost});
FROM posts) AS a ORDER BY (a.upvotes - a.downvotes) DESC LIMIT $1 OFFSET $2;
`, return results.map((p) => new Post(p));
values: [first, offset],
});
const posts = [];
for (const row of results) {
posts.push(new Post(row.id, row));
}
return posts;
} }
} }
@ -169,11 +130,8 @@ namespace dataaccess {
*/ */
export async function createPost(content: string, authorId: number, type?: string): Promise<Post> { export async function createPost(content: string, authorId: number, type?: string): Promise<Post> {
type = type || "MISC"; type = type || "MISC";
const result = await queryHelper.first({ const sqPost = await models.SqPost.create({content, userId: authorId});
text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *", const post = new Post(sqPost);
values: [content, authorId, type],
});
const post = new Post(result.id, result);
globals.internalEmitter.emit(InternalEvents.POSTCREATE, post); globals.internalEmitter.emit(InternalEvents.POSTCREATE, post);
return post; return post;
} }
@ -183,10 +141,7 @@ namespace dataaccess {
* @param postId * @param postId
*/ */
export async function deletePost(postId: number): Promise<boolean> { export async function deletePost(postId: number): Promise<boolean> {
const result = await queryHelper.first({ await (await models.SqPost.findByPk(postId)).destroy();
text: "DELETE FROM posts WHERE posts.id = $1",
values: [postId],
});
return true; return true;
} }
@ -195,31 +150,15 @@ namespace dataaccess {
* @param members * @param members
*/ */
export async function createChat(...members: number[]): Promise<Chatroom> { export async function createChat(...members: number[]): Promise<Chatroom> {
const idResult = await queryHelper.first({ return sequelize.transaction(async (t) => {
text: "INSERT INTO chats (id) values (default) RETURNING *;", const chat = await models.SqChat.create({}, {transaction: t});
});
const id = idResult.id;
const transaction = await queryHelper.createTransaction();
try {
await transaction.begin();
for (const member of members) { for (const member of members) {
await transaction.query({ await chat.addMember(Number(member), {transaction: t});
name: "chat-member-insert",
text: "INSERT INTO chat_members (chat, member) VALUES ($1, $2);",
values: [id, member],
});
} }
await transaction.commit(); const chatroom = new Chatroom(chat);
} catch (err) { globals.internalEmitter.emit(InternalEvents.CHATCREATE, chatroom);
globals.logger.warn(`Failed to insert chatmember into database: ${err.message}`); return chatroom;
globals.logger.debug(err.stack); });
await transaction.rollback();
} finally {
transaction.release();
}
const chat = new Chatroom(id);
globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat);
return chat;
} }
/** /**
@ -229,15 +168,11 @@ namespace dataaccess {
* @param content * @param content
*/ */
export async function sendChatMessage(authorId: number, chatId: number, content: string) { export async function sendChatMessage(authorId: number, chatId: number, content: string) {
const chat = new Chatroom(chatId); const chat = await models.SqChat.findByPk(chatId);
if ((await chat.exists())) { if (chat) {
const result = await queryHelper.first({ const message = await chat.createMessage({content, userId: authorId});
text: "INSERT INTO chat_messages (chat, author, content) values ($1, $2, $3) RETURNING *", globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message.message);
values: [chatId, authorId, content], return message.message;
});
const message = new ChatMessage(new User(result.author), chat, result.created_at, result.content);
globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message);
return message;
} else { } else {
throw new ChatNotFoundError(chatId); throw new ChatNotFoundError(chatId);
} }
@ -247,30 +182,20 @@ namespace dataaccess {
* Returns all chats. * Returns all chats.
*/ */
export async function getAllChats(): Promise<Chatroom[]> { export async function getAllChats(): Promise<Chatroom[]> {
const result = await queryHelper.all({ const chats = await models.SqChat.findAll();
text: "SELECT id FROM chats;", return chats.map((c) => new Chatroom(c));
});
const chats = [];
for (const row of result) {
chats.push(new Chatroom(row.id));
}
return chats;
} }
/** /**
* Sends a request to a user. * Sends a request to a user.
* @param sender * @param sender
* @param receiver * @param receiver
* @param type * @param requestType
*/ */
export async function createRequest(sender: number, receiver: number, type?: RequestType) { export async function createRequest(sender: number, receiver: number, requestType?: RequestType) {
type = type || RequestType.FRIENDREQUEST; requestType = requestType || RequestType.FRIENDREQUEST;
const result = await queryHelper.first({ const request = await models.SqRequest.create({senderId: sender, receiverId: receiver, requestType});
text: "INSERT INTO requests (sender, receiver, type) VALUES ($1, $2, $3) RETURNING *",
values: [sender, receiver, type],
});
const request = new Request(new User(result.sender), new User(result.receiver), result.type);
globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, Request); globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, Request);
return request; return request;
} }

@ -0,0 +1,5 @@
export {User} from "./User";
export {Chatroom} from "./Chatroom";
export {Post} from "./Post";
export {Profile} from "./Profile";
export {ChatMessage} from "./ChatMessage";

@ -1,5 +1,4 @@
import {GraphQLError} from "graphql"; import {GraphQLError} from "graphql";
import {BaseError} from "./BaseError";
export class NotLoggedInGqlError extends GraphQLError { export class NotLoggedInGqlError extends GraphQLError {

@ -35,7 +35,7 @@ namespace globals {
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp(), winston.format.timestamp(),
winston.format.colorize(), winston.format.colorize(),
winston.format.printf(({ level, message, timestamp }) => { winston.format.printf(({level, message, timestamp}) => {
return `${timestamp} ${level}: ${message}`; return `${timestamp} ${level}: ${message}`;
}), }),
), ),

@ -3,8 +3,8 @@ import {Namespace, Server} from "socket.io";
import dataaccess from "../lib/dataaccess"; import dataaccess from "../lib/dataaccess";
import {ChatMessage} from "../lib/dataaccess/ChatMessage"; import {ChatMessage} from "../lib/dataaccess/ChatMessage";
import {Chatroom} from "../lib/dataaccess/Chatroom"; import {Chatroom} from "../lib/dataaccess/Chatroom";
import {Request} from "../lib/dataaccess/datamodels/models";
import {Post} from "../lib/dataaccess/Post"; import {Post} from "../lib/dataaccess/Post";
import {Request} from "../lib/dataaccess/Request";
import globals from "../lib/globals"; import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents"; import {InternalEvents} from "../lib/InternalEvents";
import Route from "../lib/Route"; import Route from "../lib/Route";
@ -37,18 +37,18 @@ class HomeRoute extends Route {
socket.on("postCreate", async (content) => { socket.on("postCreate", async (content) => {
if (socket.handshake.session.userId) { if (socket.handshake.session.userId) {
const post = await dataaccess.createPost(content, socket.handshake.session.userId); const post = await dataaccess.createPost(content, socket.handshake.session.userId);
io.emit("post", await post.resolvedData()); io.emit("post", Object.assign(post, {htmlContent: post.htmlContent()}));
} else { } else {
socket.emit("error", "Not logged in!"); socket.emit("error", "Not logged in!");
} }
}); });
globals.internalEmitter.on(InternalEvents.REQUESTCREATE, (request: Request) => { globals.internalEmitter.on(InternalEvents.REQUESTCREATE, async (request: Request) => {
if (request.receiver.id === socket.handshake.session.userId) { if ((await request.getSender()).id === socket.handshake.session.userId) {
socket.emit("request", request.resolvedData()); socket.emit("request", request);
} }
}); });
globals.internalEmitter.on(InternalEvents.GQLPOSTCREATE, async (post: Post) => { globals.internalEmitter.on(InternalEvents.GQLPOSTCREATE, async (post: Post) => {
socket.emit("post", await post.resolvedData()); socket.emit("post", Object.assign(post, {htmlContent: post.htmlContent()}));
}); });
}); });
@ -82,15 +82,15 @@ class HomeRoute extends Route {
if (socket.handshake.session.userId) { if (socket.handshake.session.userId) {
const userId = socket.handshake.session.userId; const userId = socket.handshake.session.userId;
const message = await dataaccess.sendChatMessage(userId, chatId, content); const message = await dataaccess.sendChatMessage(userId, chatId, content);
socket.broadcast.emit("chatMessage", message.resolvedContent()); socket.broadcast.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent()}));
socket.emit("chatMessageSent", message.resolvedContent()); socket.emit("chatMessageSent", Object.assign(message, {htmlContent: message.htmlContent()}));
} else { } else {
socket.emit("error", "Not logged in!"); socket.emit("error", "Not logged in!");
} }
}); });
globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, (message: ChatMessage) => { globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, async (message: ChatMessage) => {
if (message.chat.id === chatId) { if ((await message.chat()).id === chatId) {
socket.emit("chatMessage", message.resolvedContent()); socket.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent()}));
} }
}); });
}); });

@ -4,6 +4,7 @@
"noImplicitAny": true, "noImplicitAny": true,
"removeComments": true, "removeComments": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"allowSyntheticDefaultImports": true,
"outDir": "./dist", "outDir": "./dist",
"sourceMap": true, "sourceMap": true,
"target": "es2018", "target": "es2018",
@ -18,4 +19,4 @@
"node_modules", "node_modules",
"**/*.spec.ts" "**/*.spec.ts"
] ]
} }

@ -21,7 +21,8 @@
}, },
"no-namespace": false, "no-namespace": false,
"no-internal-module": false, "no-internal-module": false,
"max-classes-per-file": false "max-classes-per-file": false,
"no-var-requires": false
}, },
"jsRules": { "jsRules": {
"max-line-length": { "max-line-length": {

Loading…
Cancel
Save