Merge branch 'feature/sequelize' into develop
commit
54c94f4749
File diff suppressed because it is too large
Load Diff
@ -1,73 +0,0 @@
|
||||
import * as crypto from "crypto";
|
||||
import {EventEmitter} from "events";
|
||||
|
||||
export class MemoryCache extends EventEmitter {
|
||||
private cacheItems: any = {};
|
||||
private cacheExpires: any = {};
|
||||
private expireCheck: NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
* Creates interval function.
|
||||
* @param ttl
|
||||
*/
|
||||
constructor(private ttl: number = 500) {
|
||||
super();
|
||||
this.expireCheck = setInterval(() => this.checkExpires(), ttl / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a md5 hash of the given key.
|
||||
* @param key
|
||||
*/
|
||||
public hashKey(key: string): string {
|
||||
const hash = crypto.createHash("sha1");
|
||||
const data = hash.update(key, "utf8");
|
||||
return data.digest("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an entry.
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
public set(key: string, value: any) {
|
||||
this.cacheItems[key] = value;
|
||||
this.cacheExpires[key] = Date.now() + this.ttl;
|
||||
this.emit("set", key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entry stored with the given key.
|
||||
* @param key
|
||||
*/
|
||||
public get(key: string) {
|
||||
if (this.cacheItems.hasOwnProperty(key)) {
|
||||
this.emit("hit", key, this.cacheItems[key]);
|
||||
return this.cacheItems[key];
|
||||
} else {
|
||||
this.emit("miss", key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a cache item.
|
||||
* @param key
|
||||
*/
|
||||
public delete(key: string) {
|
||||
this.emit("delete", key);
|
||||
delete this.cacheItems[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks expires and clears items that are over the expire value.
|
||||
*/
|
||||
private checkExpires() {
|
||||
for (const [key, value] of Object.entries(this.cacheExpires)) {
|
||||
if (value < Date.now()) {
|
||||
this.emit("delete", key);
|
||||
delete this.cacheItems[key];
|
||||
delete this.cacheExpires[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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(/,$/, "");
|
||||
}
|
@ -0,0 +1,230 @@
|
||||
import {Sequelize} from "sequelize-typescript";
|
||||
import {ChatNotFoundError} from "./errors/ChatNotFoundError";
|
||||
import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError";
|
||||
import {UserNotFoundError} from "./errors/UserNotFoundError";
|
||||
import globals from "./globals";
|
||||
import {InternalEvents} from "./InternalEvents";
|
||||
import * as models from "./models";
|
||||
|
||||
/**
|
||||
* Generates a new handle from the username and a base64 string of the current time.
|
||||
* @param username
|
||||
*/
|
||||
function generateHandle(username: string) {
|
||||
return `${username}.${Buffer.from(Date.now().toString()).toString("base64")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Namespace with functions to fetch initial data for wrapping.
|
||||
*/
|
||||
namespace dataaccess {
|
||||
|
||||
let sequelize: Sequelize;
|
||||
|
||||
/**
|
||||
* Initializes everything that needs to be initialized asynchronous.
|
||||
*/
|
||||
export async function init(seq: Sequelize) {
|
||||
sequelize = seq;
|
||||
try {
|
||||
await sequelize.addModels([
|
||||
models.ChatMember,
|
||||
models.ChatMessage,
|
||||
models.ChatRoom,
|
||||
models.Friendship,
|
||||
models.Post,
|
||||
models.PostVote,
|
||||
models.Request,
|
||||
models.User,
|
||||
]);
|
||||
} catch (err) {
|
||||
globals.logger.error(err.message);
|
||||
globals.logger.debug(err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user by handle.
|
||||
* @param userHandle
|
||||
*/
|
||||
export async function getUserByHandle(userHandle: string): Promise<models.User> {
|
||||
const user = await models.User.findOne({where: {handle: userHandle}});
|
||||
if (user) {
|
||||
return user;
|
||||
} else {
|
||||
throw new UserNotFoundError(userHandle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user by email and password
|
||||
* @param email
|
||||
* @param password
|
||||
*/
|
||||
export async function getUserByLogin(email: string, password: string): Promise<models.User> {
|
||||
const user = await models.User.findOne({where: {email, password}});
|
||||
if (user) {
|
||||
return user;
|
||||
} else {
|
||||
throw new UserNotFoundError(email);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a user with a username and password returning a user
|
||||
* @param username
|
||||
* @param email
|
||||
* @param password
|
||||
*/
|
||||
export async function registerUser(username: string, email: string, password: string): Promise<models.User> {
|
||||
const existResult = !!(await models.User.findOne({where: {username, email, password}}));
|
||||
const handle = generateHandle(username);
|
||||
if (!existResult) {
|
||||
return models.User.create({username, email, password, handle});
|
||||
} else {
|
||||
throw new EmailAlreadyRegisteredError(email);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a post for a given postId.s
|
||||
* @param postId
|
||||
*/
|
||||
export async function getPost(postId: number): Promise<models.Post> {
|
||||
const post = await models.Post.findByPk(postId);
|
||||
if (post) {
|
||||
return post;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all posts sorted by new or top with pagination.
|
||||
* @param first
|
||||
* @param offset
|
||||
* @param sort
|
||||
*/
|
||||
export async function getPosts(first: number, offset: number, sort: SortType) {
|
||||
if (sort === SortType.NEW) {
|
||||
return models.Post.findAll({
|
||||
include: [{association: "rVotes"}],
|
||||
limit: first,
|
||||
offset,
|
||||
order: [["createdAt", "DESC"]],
|
||||
});
|
||||
} else {
|
||||
return await sequelize.query(
|
||||
`SELECT * FROM (
|
||||
SELECT *,
|
||||
(SELECT count(*) FROM post_votes WHERE vote_type = 'UPVOTE' AND post_id = posts.id) AS upvotes ,
|
||||
(SELECT count(*) FROM post_votes WHERE vote_type = 'DOWNVOTE' AND post_id = posts.id) AS downvotes
|
||||
FROM posts) AS a ORDER BY (a.upvotes - a.downvotes) DESC LIMIT ? OFFSET ?`,
|
||||
{replacements: [first, offset], mapToModel: true, model: models.Post}) as models.Post[];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a post
|
||||
* @param content
|
||||
* @param authorId
|
||||
* @param type
|
||||
*/
|
||||
export async function createPost(content: string, authorId: number, type?: string): Promise<models.Post> {
|
||||
type = type || "MISC";
|
||||
const post = await models.Post.create({content, authorId});
|
||||
globals.internalEmitter.emit(InternalEvents.POSTCREATE, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a post
|
||||
* @param postId
|
||||
*/
|
||||
export async function deletePost(postId: number): Promise<boolean> {
|
||||
await (await models.Post.findByPk(postId)).destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a chatroom containing two users
|
||||
* @param members
|
||||
*/
|
||||
export async function createChat(...members: number[]): Promise<models.ChatRoom> {
|
||||
return sequelize.transaction(async (t) => {
|
||||
const chat = await models.ChatRoom.create({}, {transaction: t, include: [models.User]});
|
||||
for (const member of members) {
|
||||
const user = await models.User.findByPk(member);
|
||||
await chat.$add("rMember", user, {transaction: t});
|
||||
}
|
||||
await chat.save({transaction: t});
|
||||
globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat);
|
||||
return chat;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message into a chat.
|
||||
* @param authorId
|
||||
* @param chatId
|
||||
* @param content
|
||||
*/
|
||||
export async function sendChatMessage(authorId: number, chatId: number, content: string) {
|
||||
const chat = await models.ChatRoom.findByPk(chatId);
|
||||
if (chat) {
|
||||
const message = await chat.$create("rMessage", {content, authorId}) as models.ChatMessage;
|
||||
globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message);
|
||||
return message;
|
||||
} else {
|
||||
throw new ChatNotFoundError(chatId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all rChats.
|
||||
*/
|
||||
export async function getAllChats(): Promise<models.ChatRoom[]> {
|
||||
return models.ChatRoom.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to a user.
|
||||
* @param sender
|
||||
* @param receiver
|
||||
* @param requestType
|
||||
*/
|
||||
export async function createRequest(sender: number, receiver: number, requestType?: RequestType) {
|
||||
requestType = requestType || RequestType.FRIENDREQUEST;
|
||||
|
||||
const request = await models.Request.create({senderId: sender, receiverId: receiver, requestType});
|
||||
globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, Request);
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing the types of votes that can be performed on a post.
|
||||
*/
|
||||
export enum VoteType {
|
||||
UPVOTE = "UPVOTE",
|
||||
DOWNVOTE = "DOWNVOTE",
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing the types of request that can be created.
|
||||
*/
|
||||
export enum RequestType {
|
||||
FRIENDREQUEST = "FRIENDREQUEST",
|
||||
GROUPINVITE = "GROUPINVITE",
|
||||
EVENTINVITE = "EVENTINVITE",
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing the types of sorting in the feed.
|
||||
*/
|
||||
export enum SortType {
|
||||
TOP = "TOP",
|
||||
NEW = "NEW",
|
||||
}
|
||||
}
|
||||
|
||||
export default dataaccess;
|
@ -1,31 +0,0 @@
|
||||
import markdown from "../markdown";
|
||||
import {Chatroom} from "./Chatroom";
|
||||
import {User} from "./User";
|
||||
|
||||
export class ChatMessage {
|
||||
constructor(
|
||||
public readonly author: User,
|
||||
public readonly chat: Chatroom,
|
||||
public readonly createdAt: number,
|
||||
public readonly content: string) {}
|
||||
|
||||
/**
|
||||
* The content rendered by markdown-it.
|
||||
*/
|
||||
public htmlContent(): string {
|
||||
return markdown.renderInline(this.content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns resolved and rendered content of the chat message.
|
||||
*/
|
||||
public resolvedContent() {
|
||||
return {
|
||||
author: this.author.id,
|
||||
chat: this.chat.id,
|
||||
content: this.content,
|
||||
createdAt: this.createdAt,
|
||||
htmlContent: this.htmlContent(),
|
||||
};
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
import globals from "../globals";
|
||||
import {ChatMessage} from "./ChatMessage";
|
||||
import {queryHelper} from "./index";
|
||||
import {User} from "./User";
|
||||
|
||||
export class Chatroom {
|
||||
|
||||
public namespace: string;
|
||||
constructor(public readonly id: number) {
|
||||
this.id = Number(id);
|
||||
this.namespace = `/chat/${id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the chat exists.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
public async members(): Promise<User[]> {
|
||||
const result = await queryHelper.all({
|
||||
cache: true,
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns messages of the chat
|
||||
* @param limit - the limit of messages to return
|
||||
* @param offset - the offset of messages to return
|
||||
* @param containing - filter by containing
|
||||
*/
|
||||
public async messages({first, offset, containing}: {first?: number, offset?: number, containing?: string}) {
|
||||
const lim = first || 16;
|
||||
const offs = offset || 0;
|
||||
|
||||
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) {
|
||||
return messages.filter((x) => x.content.includes(containing));
|
||||
} else {
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
}
|
@ -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,159 +0,0 @@
|
||||
import markdown from "../markdown";
|
||||
import {DataObject} from "./DataObject";
|
||||
import {queryHelper} from "./index";
|
||||
import dataaccess from "./index";
|
||||
import {User} from "./User";
|
||||
|
||||
export class Post extends DataObject {
|
||||
public readonly id: number;
|
||||
private $createdAt: string;
|
||||
private $content: string;
|
||||
private $author: number;
|
||||
private $type: string;
|
||||
|
||||
/**
|
||||
* Returns the resolved data of the post.
|
||||
*/
|
||||
public async resolvedData() {
|
||||
await this.loadDataIfNotExists();
|
||||
return {
|
||||
authorId: this.$author,
|
||||
content: this.$content,
|
||||
createdAt: this.$createdAt,
|
||||
id: this.id,
|
||||
type: this.$type,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Returns the upvotes of a post.
|
||||
*/
|
||||
public async upvotes(): Promise<number> {
|
||||
const result = await queryHelper.first({
|
||||
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
|
||||
*/
|
||||
public async downvotes(): Promise<number> {
|
||||
const result = await queryHelper.first({
|
||||
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.
|
||||
*/
|
||||
public async htmlContent(): Promise<string> {
|
||||
await this.loadDataIfNotExists();
|
||||
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.
|
||||
*/
|
||||
public async author(): Promise<User> {
|
||||
await this.loadDataIfNotExists();
|
||||
return new User(this.$author);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the post.
|
||||
*/
|
||||
public async delete(): Promise<void> {
|
||||
const query = await queryHelper.first({
|
||||
text: "DELETE FROM posts WHERE id = $1",
|
||||
values: [this.id],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of vote the user performed on the post.
|
||||
*/
|
||||
public async userVote(userId: number): Promise<dataaccess.VoteType> {
|
||||
const result = await queryHelper.first({
|
||||
cache: true,
|
||||
text: "SELECT vote_type FROM votes WHERE user_id = $1 AND item_id = $2",
|
||||
values: [userId, this.id],
|
||||
});
|
||||
if (result) {
|
||||
return result.vote_type;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = $2 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.
|
||||
*/
|
||||
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,163 +0,0 @@
|
||||
import {RequestNotFoundError} from "../errors/RequestNotFoundError";
|
||||
import {Chatroom} from "./Chatroom";
|
||||
import dataaccess, {queryHelper} from "./index";
|
||||
import {User} from "./User";
|
||||
import {Request} from "./Request";
|
||||
|
||||
export class Profile extends User {
|
||||
|
||||
/**
|
||||
* Returns all chatrooms (with pagination).
|
||||
* Skips the query if the user doesn't exist.
|
||||
* @param first
|
||||
* @param offset
|
||||
*/
|
||||
public async chats({first, offset}: {first: number, offset?: number}): Promise<Chatroom[]> {
|
||||
if (!(await this.exists())) {
|
||||
return [];
|
||||
}
|
||||
first = first || 10;
|
||||
offset = offset || 0;
|
||||
|
||||
const result = await queryHelper.all({
|
||||
text: "SELECT chat FROM chat_members WHERE member = $1 LIMIT $2 OFFSET $3",
|
||||
values: [this.id, first, offset],
|
||||
});
|
||||
if (result) {
|
||||
return result.map((row) => new Chatroom(row.chat));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all open requests the user has send.
|
||||
*/
|
||||
public async sentRequests() {
|
||||
const result = await queryHelper.all({
|
||||
cache: true,
|
||||
text: "SELECT * FROM requests WHERE sender = $1",
|
||||
values: [this.id],
|
||||
});
|
||||
return this.getRequests(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all received requests of the user.
|
||||
*/
|
||||
public async receivedRequests() {
|
||||
const result = await queryHelper.all({
|
||||
cache: true,
|
||||
text: "SELECT * FROM requests WHERE receiver = $1",
|
||||
values: [this.id],
|
||||
});
|
||||
return this.getRequests(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the greenpoints of a user.
|
||||
* @param points
|
||||
*/
|
||||
public async setGreenpoints(points: number): Promise<number> {
|
||||
const result = await queryHelper.first({
|
||||
text: "UPDATE users SET greenpoints = $1 WHERE id = $2 RETURNING greenpoints",
|
||||
values: [points, this.id],
|
||||
});
|
||||
return result.greenpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the email of the user
|
||||
* @param email
|
||||
*/
|
||||
public async setEmail(email: string): Promise<string> {
|
||||
const result = await queryHelper.first({
|
||||
text: "UPDATE users SET email = $1 WHERE users.id = $2 RETURNING email",
|
||||
values: [email, this.id],
|
||||
});
|
||||
return result.email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the handle of the user
|
||||
*/
|
||||
public async setHandle(handle: string): Promise<string> {
|
||||
const result = await queryHelper.first({
|
||||
text: "UPDATE users SET handle = $1 WHERE id = $2",
|
||||
values: [handle, this.id],
|
||||
});
|
||||
return result.handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the username of the user
|
||||
* @param name
|
||||
*/
|
||||
public async setName(name: string): Promise<string> {
|
||||
const result = await queryHelper.first({
|
||||
text: "UPDATE users SET name = $1 WHERE id = $2",
|
||||
values: [name, this.id],
|
||||
});
|
||||
return result.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Denys a request.
|
||||
* @param sender
|
||||
* @param type
|
||||
*/
|
||||
public async denyRequest(sender: number, type: dataaccess.RequestType) {
|
||||
await queryHelper.first({
|
||||
text: "DELETE FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3",
|
||||
values: [this.id, sender, type],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a request.
|
||||
* @param sender
|
||||
* @param type
|
||||
*/
|
||||
public async acceptRequest(sender: number, type: dataaccess.RequestType) {
|
||||
const exists = await queryHelper.first({
|
||||
cache: true,
|
||||
text: "SELECT 1 FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3",
|
||||
values: [this.id, sender, type],
|
||||
});
|
||||
if (exists) {
|
||||
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 {
|
||||
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,128 +0,0 @@
|
||||
import globals from "../globals";
|
||||
import {DataObject} from "./DataObject";
|
||||
import {queryHelper} from "./index";
|
||||
import {Post} from "./Post";
|
||||
|
||||
export class User extends DataObject {
|
||||
private $name: string;
|
||||
private $handle: string;
|
||||
private $email: string;
|
||||
private $greenpoints: number;
|
||||
private $joinedAt: string;
|
||||
private $exists: boolean;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* The email of the user
|
||||
*/
|
||||
public async email(): Promise<string> {
|
||||
await this.loadDataIfNotExists();
|
||||
return this.$email;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public async numberOfPosts(): Promise<number> {
|
||||
const result = await queryHelper.first({
|
||||
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.
|
||||
*/
|
||||
public async friends(): Promise<User[]> {
|
||||
const result = await queryHelper.all({
|
||||
cache: true,
|
||||
text: "SELECT * FROM user_friends WHERE user_id = $1 OR friend_id = $1",
|
||||
values: [this.id],
|
||||
});
|
||||
const userFriends = [];
|
||||
for (const row of result) {
|
||||
if (row.user_id === this.id) {
|
||||
userFriends.push(new User(row.friend_id));
|
||||
} else {
|
||||
userFriends.push(new User(row.user_id));
|
||||
}
|
||||
}
|
||||
return userFriends;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all posts for a user.
|
||||
*/
|
||||
public async posts({first, offset}: {first: number, offset: number}): Promise<Post[]> {
|
||||
first = first || 10;
|
||||
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 = [];
|
||||
|
||||
for (const row of result) {
|
||||
posts.push(new Post(row.id, row));
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,304 +0,0 @@
|
||||
import {Pool} from "pg";
|
||||
import {ChatNotFoundError} from "../errors/ChatNotFoundError";
|
||||
import {EmailAlreadyRegisteredError} from "../errors/EmailAlreadyRegisteredError";
|
||||
import {UserNotFoundError} from "../errors/UserNotFoundError";
|
||||
import globals from "../globals";
|
||||
import {InternalEvents} from "../InternalEvents";
|
||||
import {QueryHelper} from "../QueryHelper";
|
||||
import {ChatMessage} from "./ChatMessage";
|
||||
import {Chatroom} from "./Chatroom";
|
||||
import {Post} from "./Post";
|
||||
import {Profile} from "./Profile";
|
||||
import {Request} from "./Request";
|
||||
import {User} from "./User";
|
||||
|
||||
const config = globals.config;
|
||||
const tableCreationFile = __dirname + "/../../sql/create-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.
|
||||
* @param username
|
||||
*/
|
||||
function generateHandle(username: string) {
|
||||
return `${username}.${Buffer.from(Date.now().toString()).toString("base64")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Namespace with functions to fetch initial data for wrapping.
|
||||
*/
|
||||
namespace dataaccess {
|
||||
|
||||
export const pool: Pool = dbClient;
|
||||
|
||||
/**
|
||||
* Initializes everything that needs to be initialized asynchronous.
|
||||
*/
|
||||
export async function init() {
|
||||
try {
|
||||
await queryHelper.init();
|
||||
} catch (err) {
|
||||
globals.logger.error(err.message);
|
||||
globals.logger.debug(err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user by handle.
|
||||
* @param userHandle
|
||||
*/
|
||||
export async function getUserByHandle(userHandle: string): Promise<User> {
|
||||
const result = await queryHelper.first({
|
||||
text: "SELECT * FROM users WHERE users.handle = $1",
|
||||
values: [userHandle],
|
||||
});
|
||||
if (result) {
|
||||
return new User(result.id, result);
|
||||
} else {
|
||||
throw new UserNotFoundError(userHandle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user by email and password
|
||||
* @param email
|
||||
* @param password
|
||||
*/
|
||||
export async function getUserByLogin(email: string, password: string): Promise<Profile> {
|
||||
const result = await queryHelper.first({
|
||||
text: "SELECT * FROM users WHERE email = $1 AND password = $2",
|
||||
values: [email, password],
|
||||
});
|
||||
if (result) {
|
||||
return new Profile(result.id, result);
|
||||
} else {
|
||||
throw new UserNotFoundError(email);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a user with a username and password returning a user
|
||||
* @param username
|
||||
* @param email
|
||||
* @param password
|
||||
*/
|
||||
export async function registerUser(username: string, email: string, password: string) {
|
||||
const existResult = await queryHelper.first({
|
||||
text: "SELECT email FROM users WHERE email = $1;",
|
||||
values: [email],
|
||||
});
|
||||
if (!existResult || !existResult.email) {
|
||||
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 {
|
||||
throw new EmailAlreadyRegisteredError(email);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a post for a given postId.s
|
||||
* @param postId
|
||||
*/
|
||||
export async function getPost(postId: number): Promise<Post> {
|
||||
const result = await queryHelper.first({
|
||||
text: "SELECT * FROM posts WHERE id = $1",
|
||||
values: [postId],
|
||||
});
|
||||
if (result) {
|
||||
return new Post(result.id, result);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all posts sorted by new or top with pagination.
|
||||
* @param first
|
||||
* @param offset
|
||||
* @param sort
|
||||
*/
|
||||
export async function getPosts(first: number, offset: number, sort: SortType) {
|
||||
if (sort === SortType.NEW) {
|
||||
const results = await queryHelper.all({
|
||||
cache: true,
|
||||
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 {
|
||||
const results = await queryHelper.all({
|
||||
cache: true,
|
||||
text: `
|
||||
SELECT * FROM (
|
||||
SELECT *,
|
||||
(SELECT count(*) FROM votes WHERE vote_type = 'UPVOTE' AND item_id = posts.id) AS upvotes ,
|
||||
(SELECT count(*) FROM votes WHERE vote_type = 'DOWNVOTE' AND item_id = posts.id) AS downvotes
|
||||
FROM posts) AS a ORDER BY (a.upvotes - a.downvotes) DESC LIMIT $1 OFFSET $2;
|
||||
`,
|
||||
values: [first, offset],
|
||||
});
|
||||
const posts = [];
|
||||
for (const row of results) {
|
||||
posts.push(new Post(row.id, row));
|
||||
}
|
||||
return posts;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a post
|
||||
* @param content
|
||||
* @param authorId
|
||||
* @param type
|
||||
*/
|
||||
export async function createPost(content: string, authorId: number, type?: string): Promise<Post> {
|
||||
type = type || "MISC";
|
||||
const result = await queryHelper.first({
|
||||
text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *",
|
||||
values: [content, authorId, type],
|
||||
});
|
||||
const post = new Post(result.id, result);
|
||||
globals.internalEmitter.emit(InternalEvents.POSTCREATE, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a post
|
||||
* @param postId
|
||||
*/
|
||||
export async function deletePost(postId: number): Promise<boolean> {
|
||||
const result = await queryHelper.first({
|
||||
text: "DELETE FROM posts WHERE posts.id = $1",
|
||||
values: [postId],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a chatroom containing two users
|
||||
* @param members
|
||||
*/
|
||||
export async function createChat(...members: number[]): Promise<Chatroom> {
|
||||
const idResult = await queryHelper.first({
|
||||
text: "INSERT INTO chats (id) values (default) RETURNING *;",
|
||||
});
|
||||
const id = idResult.id;
|
||||
const transaction = await queryHelper.createTransaction();
|
||||
try {
|
||||
await transaction.begin();
|
||||
for (const member of members) {
|
||||
await transaction.query({
|
||||
name: "chat-member-insert",
|
||||
text: "INSERT INTO chat_members (chat, member) VALUES ($1, $2);",
|
||||
values: [id, member],
|
||||
});
|
||||
}
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
globals.logger.warn(`Failed to insert chatmember into database: ${err.message}`);
|
||||
globals.logger.debug(err.stack);
|
||||
await transaction.rollback();
|
||||
} finally {
|
||||
transaction.release();
|
||||
}
|
||||
const chat = new Chatroom(id);
|
||||
globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat);
|
||||
return chat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message into a chat.
|
||||
* @param authorId
|
||||
* @param chatId
|
||||
* @param content
|
||||
*/
|
||||
export async function sendChatMessage(authorId: number, chatId: number, content: string) {
|
||||
const chat = new Chatroom(chatId);
|
||||
if ((await chat.exists())) {
|
||||
const result = await queryHelper.first({
|
||||
text: "INSERT INTO chat_messages (chat, author, content) values ($1, $2, $3) RETURNING *",
|
||||
values: [chatId, authorId, content],
|
||||
});
|
||||
const message = new ChatMessage(new User(result.author), chat, result.created_at, result.content);
|
||||
globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message);
|
||||
return message;
|
||||
} else {
|
||||
throw new ChatNotFoundError(chatId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all chats.
|
||||
*/
|
||||
export async function getAllChats(): Promise<Chatroom[]> {
|
||||
const result = await queryHelper.all({
|
||||
text: "SELECT id FROM chats;",
|
||||
});
|
||||
const chats = [];
|
||||
for (const row of result) {
|
||||
chats.push(new Chatroom(row.id));
|
||||
}
|
||||
return chats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to a user.
|
||||
* @param sender
|
||||
* @param receiver
|
||||
* @param type
|
||||
*/
|
||||
export async function createRequest(sender: number, receiver: number, type?: RequestType) {
|
||||
type = type || RequestType.FRIENDREQUEST;
|
||||
|
||||
const result = await queryHelper.first({
|
||||
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);
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing the types of votes that can be performed on a post.
|
||||
*/
|
||||
export enum VoteType {
|
||||
UPVOTE = "UPVOTE",
|
||||
DOWNVOTE = "DOWNVOTE",
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing the types of request that can be created.
|
||||
*/
|
||||
export enum RequestType {
|
||||
FRIENDREQUEST = "FRIENDREQUEST",
|
||||
GROUPINVITE = "GROUPINVITE",
|
||||
EVENTINVITE = "EVENTINVITE",
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing the types of sorting in the feed.
|
||||
*/
|
||||
export enum SortType {
|
||||
TOP = "TOP",
|
||||
NEW = "NEW",
|
||||
}
|
||||
}
|
||||
|
||||
export default dataaccess;
|
@ -1,9 +1,13 @@
|
||||
import {GraphQLError} from "graphql";
|
||||
import {BaseError} from "./BaseError";
|
||||
|
||||
export class NotLoggedInGqlError extends GraphQLError {
|
||||
|
||||
constructor() {
|
||||
super("Not logged in");
|
||||
}
|
||||
}
|
||||
|
||||
export class PostNotFoundGqlError extends GraphQLError {
|
||||
constructor(postId: number) {
|
||||
super(`Post '${postId}' not found!`);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||
import {ChatRoom} from "./ChatRoom";
|
||||
import {User} from "./User";
|
||||
|
||||
@Table({underscored: true})
|
||||
export class ChatMember extends Model<ChatMember> {
|
||||
@ForeignKey(() => User)
|
||||
@NotNull
|
||||
@Column({allowNull: false})
|
||||
public userId: number;
|
||||
|
||||
@ForeignKey(() => ChatRoom)
|
||||
@NotNull
|
||||
@Column({allowNull: false})
|
||||
public chatId: number;
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import * as sqz from "sequelize";
|
||||
import {BelongsTo, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||
import markdown from "../markdown";
|
||||
import {ChatRoom} from "./ChatRoom";
|
||||
import {User} from "./User";
|
||||
|
||||
@Table({underscored: true})
|
||||
export class ChatMessage extends Model<ChatMessage> {
|
||||
|
||||
@NotNull
|
||||
@Column({type: sqz.STRING(512), allowNull: false})
|
||||
public content: string;
|
||||
|
||||
@ForeignKey(() => ChatRoom)
|
||||
@NotNull
|
||||
@Column({allowNull: false})
|
||||
public chatId: number;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@NotNull
|
||||
@Column({allowNull: false})
|
||||
public authorId: number;
|
||||
|
||||
@BelongsTo(() => ChatRoom, "chatId")
|
||||
public rChat: ChatRoom;
|
||||
|
||||
@BelongsTo(() => User, "authorId")
|
||||
public rAuthor: User;
|
||||
|
||||
@CreatedAt
|
||||
public createdAt: Date;
|
||||
|
||||
public async chat(): Promise<ChatRoom> {
|
||||
return await this.$get("rChat") as ChatRoom;
|
||||
}
|
||||
|
||||
public async author(): Promise<User> {
|
||||
return await this.$get("rAuthor") as User;
|
||||
}
|
||||
|
||||
public get htmlContent(): string {
|
||||
return markdown.renderInline(this.getDataValue("content"));
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import {BelongsToMany, CreatedAt, HasMany, Model, Table,} from "sequelize-typescript";
|
||||
import {ChatMember} from "./ChatMember";
|
||||
import {ChatMessage} from "./ChatMessage";
|
||||
import {User} from "./User";
|
||||
|
||||
@Table({underscored: true})
|
||||
export class ChatRoom extends Model<ChatRoom> {
|
||||
@BelongsToMany(() => User, () => ChatMember)
|
||||
public rMembers: User[];
|
||||
|
||||
@HasMany(() => ChatMessage, "chatId")
|
||||
public rMessages: ChatMessage[];
|
||||
|
||||
@CreatedAt
|
||||
public readonly createdAt!: Date;
|
||||
|
||||
public async members(): Promise<User[]> {
|
||||
return await this.$get("rMembers") as User[];
|
||||
}
|
||||
|
||||
public async messages(): Promise<ChatMessage[]> {
|
||||
return await this.$get("rMessages") as ChatMessage[];
|
||||
}
|
||||
|
||||
public get namespace(): string {
|
||||
return "/chats/" + this.getDataValue("id");
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import {Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
|
||||
import {User} from "./User";
|
||||
|
||||
@Table({underscored: true})
|
||||
export class Friendship extends Model<Friendship> {
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@NotNull
|
||||
@Column({allowNull: false})
|
||||
public userId: number;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@NotNull
|
||||
@Column({allowNull: false})
|
||||
public friendId: number;
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import * as sqz from "sequelize";
|
||||
import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||
import markdown from "../markdown";
|
||||
import {PostVote, VoteType} from "./PostVote";
|
||||
import {User} from "./User";
|
||||
|
||||
@Table({underscored: true})
|
||||
export class Post extends Model<Post> {
|
||||
@NotNull
|
||||
@Column({type: sqz.STRING(2048), allowNull: false})
|
||||
public content: string;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@NotNull
|
||||
@Column({allowNull: false})
|
||||
public authorId: number;
|
||||
|
||||
@BelongsTo(() => User, "authorId")
|
||||
public rAuthor: User;
|
||||
|
||||
@BelongsToMany(() => User, () => PostVote)
|
||||
public rVotes: Array<User & {PostVote: PostVote}>;
|
||||
|
||||
@CreatedAt
|
||||
public readonly createdAt!: Date;
|
||||
|
||||
public async author(): Promise<User> {
|
||||
return await this.$get("rAuthor") as User;
|
||||
}
|
||||
|
||||
public async votes(): Promise<Array<User & {PostVote: PostVote}>> {
|
||||
return await this.$get("rVotes") as Array<User & {PostVote: PostVote}>;
|
||||
}
|
||||
|
||||
public get htmlContent() {
|
||||
return markdown.render(this.getDataValue("content"));
|
||||
}
|
||||
|
||||
public async upvotes() {
|
||||
return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.UPVOTE).length;
|
||||
}
|
||||
|
||||
public async downvotes() {
|
||||
return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.DOWNVOTE).length;
|
||||
}
|
||||
|
||||
public async vote(userId: number, type: VoteType): Promise<VoteType> {
|
||||
type = type || VoteType.UPVOTE;
|
||||
let votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>;
|
||||
let vote = votes[0] || null;
|
||||
let created = false;
|
||||
if (!vote) {
|
||||
await this.$add("rVote", userId);
|
||||
votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>;
|
||||
vote = votes[0] || null;
|
||||
created = true;
|
||||
}
|
||||
if (vote) {
|
||||
if (vote.PostVote.voteType === type && !created) {
|
||||
await vote.PostVote.destroy();
|
||||
return null;
|
||||
} else {
|
||||
vote.PostVote.voteType = type;
|
||||
await vote.PostVote.save();
|
||||
}
|
||||
}
|
||||
|
||||
return vote.PostVote.voteType;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import * as sqz from "sequelize";
|
||||
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||
import {Post} from "./Post";
|
||||
import {User} from "./User";
|
||||
|
||||
export enum VoteType {
|
||||
UPVOTE = "UPVOTE",
|
||||
DOWNVOTE = "DOWNVOTE",
|
||||
}
|
||||
|
||||
@Table({underscored: true})
|
||||
export class PostVote extends Model<PostVote> {
|
||||
@NotNull
|
||||
@Column({type: sqz.ENUM, values: ["UPVOTE", "DOWNVOTE"], defaultValue: "UPVOTE", allowNull: false})
|
||||
public voteType: VoteType;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@NotNull
|
||||
@Column({allowNull: false})
|
||||
public userId: number;
|
||||
|
||||
@ForeignKey(() => Post)
|
||||
@NotNull
|
||||
@Column({allowNull: false})
|
||||
public postId: number;
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import * as sqz from "sequelize";
|
||||
import {BelongsTo, Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||
import {User} from "./User";
|
||||
|
||||
export enum RequestType {
|
||||
FRIENDREQUEST = "FRIENDREQUEST",
|
||||
GROUPINVITE = "GROUPINVITE",
|
||||
EVENTINVITE = "EVENTINVITE",
|
||||
}
|
||||
|
||||
@Table({underscored: true})
|
||||
export class Request extends Model<Request> {
|
||||
@NotNull
|
||||
@Column({type: sqz.ENUM, values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"],
|
||||
defaultValue: "FRIENDREQUEST", allowNull: false})
|
||||
public requestType: RequestType;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@NotNull
|
||||
@Column({allowNull: false})
|
||||
public senderId: number;
|
||||
|
||||
@BelongsTo(() => User, "senderId")
|
||||
public rSender: User;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@NotNull
|
||||
@Column({allowNull: false})
|
||||
public receiverId: number;
|
||||
|
||||
@BelongsTo(() => User, "receiverId")
|
||||
public rReceiver: User;
|
||||
|
||||
public async receiver(): Promise<User> {
|
||||
return await this.$get("rReceiver") as User;
|
||||
}
|
||||
|
||||
public async sender(): Promise<User> {
|
||||
return await this.$get("rSender") as User;
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
import * as sqz from "sequelize";
|
||||
import {
|
||||
BelongsToMany,
|
||||
Column,
|
||||
CreatedAt,
|
||||
HasMany,
|
||||
Model,
|
||||
NotNull,
|
||||
Table,
|
||||
Unique,
|
||||
UpdatedAt,
|
||||
} from "sequelize-typescript";
|
||||
import {RequestNotFoundError} from "../errors/RequestNotFoundError";
|
||||
import {ChatMember} from "./ChatMember";
|
||||
import {ChatMessage} from "./ChatMessage";
|
||||
import {ChatRoom} from "./ChatRoom";
|
||||
import {Friendship} from "./Friendship";
|
||||
import {Post} from "./Post";
|
||||
import {PostVote} from "./PostVote";
|
||||
import {Request, RequestType} from "./Request";
|
||||
|
||||
@Table({underscored: true})
|
||||
export class User extends Model<User> {
|
||||
@NotNull
|
||||
@Column({type: sqz.STRING(128), allowNull: false})
|
||||
public username: string;
|
||||
|
||||
@NotNull
|
||||
@Unique
|
||||
@Column({type: sqz.STRING(128), allowNull: false, unique: true})
|
||||
public handle: string;
|
||||
|
||||
@Unique
|
||||
@NotNull
|
||||
@Column({type: sqz.STRING(128), allowNull: false, unique: true})
|
||||
public email: string;
|
||||
|
||||
@NotNull
|
||||
@Column({type: sqz.STRING(128), allowNull: false})
|
||||
public password: string;
|
||||
|
||||
@NotNull
|
||||
@Column({defaultValue: 0, allowNull: false})
|
||||
public rankpoints: number;
|
||||
|
||||
@BelongsToMany(() => User, () => Friendship)
|
||||
public friends: User[];
|
||||
|
||||
@BelongsToMany(() => Post, () => PostVote)
|
||||
public votes: Array<Post & {PostVote: PostVote}>;
|
||||
|
||||
@BelongsToMany(() => ChatRoom, () => ChatMember)
|
||||
public rChats: ChatRoom[];
|
||||
|
||||
@HasMany(() => Post, "authorId")
|
||||
public rPosts: Post[];
|
||||
|
||||
@HasMany(() => Request, "receiverId")
|
||||
public rSentRequests: Request[];
|
||||
|
||||
@HasMany(() => Request, "receiverId")
|
||||
public rReceivedRequests: Request[];
|
||||
|
||||
@HasMany(() => ChatMessage, "authorId")
|
||||
public messages: ChatMessage[];
|
||||
|
||||
@CreatedAt
|
||||
public readonly createdAt!: Date;
|
||||
|
||||
@UpdatedAt
|
||||
public readonly updatedAt!: Date;
|
||||
|
||||
public get name(): string {
|
||||
return this.getDataValue("username");
|
||||
}
|
||||
|
||||
public get joinedAt(): Date {
|
||||
return this.getDataValue("createdAt");
|
||||
}
|
||||
|
||||
public async chats(): Promise<ChatRoom[]> {
|
||||
return await this.$get("rChats") as ChatRoom[];
|
||||
}
|
||||
|
||||
public async sentRequests(): Promise<Request[]> {
|
||||
return await this.$get("rSentRequests") as Request[];
|
||||
}
|
||||
|
||||
public async receivedRequests(): Promise<Request[]> {
|
||||
return await this.$get("rReceivedRequests") as Request[];
|
||||
}
|
||||
|
||||
public async posts({first, offset}: {first: number, offset: number}): Promise<Post[]> {
|
||||
return await this.$get("rPosts", {limit: first, offset}) as Post[];
|
||||
}
|
||||
|
||||
public async numberOfPosts(): Promise<number> {
|
||||
return this.$count("rPosts");
|
||||
}
|
||||
|
||||
public async denyRequest(sender: number, type: RequestType) {
|
||||
const request = await this.$get("rReceivedRequests",
|
||||
{where: {senderId: sender, requestType: type}}) as Request[];
|
||||
if (request[0]) {
|
||||
await request[0].destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public async acceptRequest(sender: number, type: RequestType) {
|
||||
const requests = await this.$get("rReceivedRequests",
|
||||
{where: {senderId: sender, requestType: type}}) as Request[];
|
||||
if (requests.length > 0) {
|
||||
const request = requests[0];
|
||||
if (request.requestType === RequestType.FRIENDREQUEST) {
|
||||
await this.$add("friends", sender);
|
||||
await request.destroy();
|
||||
}
|
||||
} else {
|
||||
throw new RequestNotFoundError(sender, this.id, type);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
export {ChatMember} from "./ChatMember";
|
||||
export {ChatMessage} from "./ChatMessage";
|
||||
export {ChatRoom} from "./ChatRoom";
|
||||
export {Friendship} from "./Friendship";
|
||||
export {Post} from "./Post";
|
||||
export {PostVote} from "./PostVote";
|
||||
export {Request} from "./Request";
|
||||
export {User} from "./User";
|
@ -1,137 +0,0 @@
|
||||
--create functions
|
||||
DO $$BEGIN
|
||||
|
||||
IF NOT EXISTS(SELECT 1 from pg_proc WHERE proname = 'function_exists') THEN
|
||||
CREATE FUNCTION function_exists(text) RETURNS boolean LANGUAGE plpgsql AS $BODY$
|
||||
BEGIN
|
||||
RETURN EXISTS(SELECT 1 from pg_proc WHERE proname = $1);
|
||||
END $BODY$;
|
||||
END IF;
|
||||
|
||||
IF NOT function_exists('type_exists') THEN
|
||||
CREATE FUNCTION type_exists(text) RETURNS boolean LANGUAGE plpgsql AS $BODY$
|
||||
BEGIN
|
||||
RETURN EXISTS (SELECT 1 FROM pg_type WHERE typname = $1);
|
||||
END $BODY$;
|
||||
END IF;
|
||||
|
||||
END$$;
|
||||
|
||||
--create types
|
||||
DO $$ BEGIN
|
||||
|
||||
IF NOT type_exists('votetype') THEN
|
||||
CREATE TYPE votetype AS enum ('DOWNVOTE', 'UPVOTE');
|
||||
END IF;
|
||||
|
||||
IF NOT type_exists('posttype') THEN
|
||||
CREATE TYPE posttype AS enum ('MISC', 'ACTION', 'IMAGE', 'TEXT');
|
||||
END IF;
|
||||
|
||||
IF NOT type_exists('requesttype') THEN
|
||||
CREATE TYPE requesttype AS enum ('FRIENDREQUEST');
|
||||
END IF;
|
||||
|
||||
END$$;
|
||||
|
||||
-- create functions relying on types
|
||||
|
||||
DO $$ BEGIN
|
||||
|
||||
IF NOT function_exists('cast_to_votetype') THEN
|
||||
CREATE FUNCTION cast_to_votetype(text) RETURNS votetype LANGUAGE plpgsql AS $BODY$
|
||||
BEGIN
|
||||
RETURN CASE WHEN $1::votetype IS NULL THEN 'UPVOTE' ELSE $1::votetype END;
|
||||
END $BODY$;
|
||||
END IF;
|
||||
|
||||
IF NOT function_exists('cast_to_posttype') THEN
|
||||
CREATE FUNCTION cast_to_posttype(text) RETURNS posttype LANGUAGE plpgsql AS $BODY$
|
||||
BEGIN
|
||||
RETURN CASE WHEN $1::posttype IS NULL THEN 'MISC' ELSE $1::posttype END;
|
||||
END $BODY$;
|
||||
END IF;
|
||||
|
||||
END$$;
|
||||
|
||||
-- create tables
|
||||
DO $$ BEGIN
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "user_sessions" (
|
||||
"sid" varchar NOT NULL,
|
||||
"sess" json NOT NULL,
|
||||
"expire" timestamp(6) NOT NULL,
|
||||
PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||
) WITH (OIDS=FALSE);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name varchar(128) NOT NULL,
|
||||
handle varchar(128) UNIQUE NOT NULL,
|
||||
password varchar(1024) NOT NULL,
|
||||
email varchar(128) UNIQUE NOT NULL,
|
||||
greenpoints INTEGER DEFAULT 0,
|
||||
joined_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
upvotes INTEGER DEFAULT 0,
|
||||
downvotes INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT now(),
|
||||
content text,
|
||||
author SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
||||
type posttype NOT NULL DEFAULT 'MISC'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS votes (
|
||||
user_id SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
||||
item_id BIGSERIAL REFERENCES posts (id) ON DELETE CASCADE,
|
||||
vote_type votetype DEFAULT 'DOWNVOTE',
|
||||
PRIMARY KEY (user_id, item_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time TIMESTAMP,
|
||||
owner SERIAL REFERENCES users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS event_members (
|
||||
event BIGSERIAL REFERENCES events (id),
|
||||
member SERIAL REFERENCES users (id),
|
||||
PRIMARY KEY (event, member)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chats (
|
||||
id BIGSERIAL PRIMARY KEY
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||
chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE,
|
||||
author SERIAL REFERENCES users (id) ON DELETE SET NULL,
|
||||
content VARCHAR(1024) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT now(),
|
||||
PRIMARY KEY (chat, author, created_at)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chat_members (
|
||||
chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE,
|
||||
member SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (chat, member)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_friends (
|
||||
user_id SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
||||
friend_id SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (user_id, friend_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS requests (
|
||||
sender SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
||||
receiver SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
||||
type requesttype DEFAULT 'FRIENDREQUEST',
|
||||
PRIMARY KEY (sender, receiver, type)
|
||||
);
|
||||
|
||||
END $$;
|
@ -1,19 +0,0 @@
|
||||
DO $$ BEGIN
|
||||
|
||||
ALTER TABLE IF EXISTS votes
|
||||
ADD COLUMN IF NOT EXISTS vote_type votetype DEFAULT 'UPVOTE',
|
||||
ALTER COLUMN vote_type TYPE votetype USING cast_to_votetype(vote_type::text),
|
||||
ALTER COLUMN vote_type DROP DEFAULT,
|
||||
ALTER COLUMN vote_type SET DEFAULT 'UPVOTE';
|
||||
|
||||
ALTER TABLE IF EXISTS posts
|
||||
ALTER COLUMN type TYPE posttype USING cast_to_posttype(type::text),
|
||||
ALTER COLUMN type DROP DEFAULT,
|
||||
ALTER COLUMN type SET DEFAULT 'MISC',
|
||||
DROP COLUMN IF EXISTS upvotes,
|
||||
DROP COLUMN IF EXISTS downvotes;
|
||||
|
||||
ALTER TABLE requests
|
||||
ADD COLUMN IF NOT EXISTS type requesttype DEFAULT 'FRIENDREQUEST';
|
||||
|
||||
END $$;
|
Loading…
Reference in New Issue