Merge branch 'develop' into julius-dev

pull/2/head
Trivernis 5 years ago
commit 8dd1edd817

2
.gitignore vendored

@ -6,3 +6,5 @@ test/*.log
dist dist
.idea .idea
config.yaml config.yaml
sqz-force
greenvironment.db

@ -13,4 +13,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- DTOs - DTOs
- Home Route - Home Route
- session management - session management
- Sequelize modules and integration - Sequelize models and integration
- Sequelize-typescript integration

47
package-lock.json generated

@ -223,9 +223,9 @@
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "12.7.8", "version": "12.7.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.8.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz",
"integrity": "sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A==" "integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ=="
}, },
"@types/pg": { "@types/pg": {
"version": "7.11.0", "version": "7.11.0",
@ -1477,14 +1477,6 @@
} }
} }
}, },
"connect-pg-simple": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-6.0.1.tgz",
"integrity": "sha512-zW5AOtRNOLcXxphSmQ+oYj0snlLs1Je3u5K2NWyF7WhMVoPvnQXraK2wzS8f7qLwhMcmYukah2ymu0Gdxf7Qsg==",
"requires": {
"pg": "^7.4.3"
}
},
"connect-session-sequelize": { "connect-session-sequelize": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/connect-session-sequelize/-/connect-session-sequelize-6.0.0.tgz", "resolved": "https://registry.npmjs.org/connect-session-sequelize/-/connect-session-sequelize-6.0.0.tgz",
@ -3185,11 +3177,6 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
}, },
"g": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/g/-/g-2.0.1.tgz",
"integrity": "sha1-C1lj69DKcOO8jGdmk0oCGCHIuFc="
},
"gauge": { "gauge": {
"version": "2.7.4", "version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@ -5830,6 +5817,11 @@
"strip-indent": "^1.0.1" "strip-indent": "^1.0.1"
} }
}, },
"reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
},
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.11.1", "version": "0.11.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
@ -6242,6 +6234,29 @@
"resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-2.3.0.tgz", "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-2.3.0.tgz",
"integrity": "sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA==" "integrity": "sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA=="
}, },
"sequelize-typescript": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/sequelize-typescript/-/sequelize-typescript-1.0.0.tgz",
"integrity": "sha512-oXyvHRTOyI8sJettpISL5LO30GaMMrLqzxiLCy6MjUmBJdaQDpdjn7ofge4J87MSdw+YPzkjrJLogMc9ONY2Tg==",
"requires": {
"glob": "7.1.2"
},
"dependencies": {
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"serve-static": { "serve-static": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",

@ -20,6 +20,7 @@
"author": "SoftEngI", "author": "SoftEngI",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/bluebird": "^3.5.27",
"@types/compression": "^1.0.1", "@types/compression": "^1.0.1",
"@types/connect-pg-simple": "^4.2.0", "@types/connect-pg-simple": "^4.2.0",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
@ -33,10 +34,11 @@
"@types/http-status": "^0.2.30", "@types/http-status": "^0.2.30",
"@types/js-yaml": "^3.12.1", "@types/js-yaml": "^3.12.1",
"@types/markdown-it": "0.0.9", "@types/markdown-it": "0.0.9",
"@types/node": "^12.7.8", "@types/node": "^12.7.12",
"@types/pg": "^7.11.0", "@types/pg": "^7.11.0",
"@types/sequelize": "^4.28.5", "@types/sequelize": "^4.28.5",
"@types/socket.io": "^2.1.2", "@types/socket.io": "^2.1.2",
"@types/validator": "^10.11.3",
"@types/winston": "^2.4.4", "@types/winston": "^2.4.4",
"delete": "^1.1.0", "delete": "^1.1.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
@ -66,7 +68,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",
"reflect-metadata": "^0.1.13",
"sequelize": "^5.19.6", "sequelize": "^5.19.6",
"sequelize-typescript": "^1.0.0",
"socket.io": "^2.2.0", "socket.io": "^2.2.0",
"sqlite3": "^4.1.0", "sqlite3": "^4.1.0",
"winston": "^3.2.1" "winston": "^3.2.1"

@ -5,17 +5,17 @@ import * as express from "express";
import * as graphqlHTTP from "express-graphql"; import * as graphqlHTTP from "express-graphql";
import * as session from "express-session"; import * as session from "express-session";
import sharedsession = require("express-socket.io-session"); import sharedsession = require("express-socket.io-session");
import * as fsx from "fs-extra";
import {buildSchema} from "graphql"; 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 {Sequelize} from "sequelize-typescript";
import * as socketIo from "socket.io"; import * as socketIo from "socket.io";
import {resolver} from "./graphql/resolvers"; import {resolver} from "./graphql/resolvers";
import dataaccess 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 SequelizeStore = require("connect-session-sequelize")(session.Store);
const logger = globals.logger; const logger = globals.logger;
@ -30,7 +30,7 @@ class App {
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); this.sequelize = new Sequelize(globals.config.database.connectionUri );
} }
/** /**

@ -1,6 +1,6 @@
# database connection info # database connection info
database: database:
connectionUri: "sqlite://:memory:" connectionUri: "sqlite://greenvironment.db"
# http server configuration # http server configuration
server: server:

@ -1,13 +1,8 @@
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 * as models from "../lib/models";
import * as models from "../lib/dataaccess/datamodels"; import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors";
import {Post} from "../lib/dataaccess/Post";
import {Profile} from "../lib/dataaccess/Profile";
import {User} from "../lib/dataaccess/User";
import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors";
import globals from "../lib/globals"; import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents"; import {InternalEvents} from "../lib/InternalEvents";
import {is} from "../lib/regex"; import {is} from "../lib/regex";
@ -21,8 +16,7 @@ export function resolver(req: any, res: any): any {
return { return {
async getSelf() { async getSelf() {
if (req.session.userId) { if (req.session.userId) {
const user = await models.SqUser.findByPk(req.session.userId); return models.User.findByPk(req.session.userId);
return user.profile;
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
@ -32,8 +26,7 @@ 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) {
const user = await models.SqUser.findByPk(userId); return models.User.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.");
@ -49,8 +42,7 @@ export function resolver(req: any, res: any): any {
}, },
async getChat({chatId}: { chatId: number }) { async getChat({chatId}: { chatId: number }) {
if (chatId) { if (chatId) {
const chat = await models.SqChat.findByPk(chatId); return models.ChatRoom.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.");
@ -110,8 +102,13 @@ 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) {
const post = await models.SqPost.findByPk(postId); const post = await models.Post.findByPk(postId);
return await post.post.vote(req.session.userId, type); if (post) {
return await post.vote(req.session.userId, type);
} else {
res.status(400);
return new PostNotFoundGqlError(postId);
}
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
@ -138,8 +135,8 @@ export function resolver(req: any, res: any): any {
}, },
async deletePost({postId}: { postId: number }) { async deletePost({postId}: { postId: number }) {
if (postId) { if (postId) {
const post = (await models.SqPost.findByPk(postId)).post; const post = await models.Post.findByPk(postId, {include: [models.User]});
if ((await post.author()).id === req.session.userId) { if (post.rAuthor.id === req.session.userId) {
return await dataaccess.deletePost(post.id); return await dataaccess.deletePost(post.id);
} else { } else {
res.status(status.FORBIDDEN); res.status(status.FORBIDDEN);
@ -200,8 +197,8 @@ export function resolver(req: any, res: any): any {
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
} }
if (sender && type) { if (sender && type) {
const profile = new Profile(req.session.userId); const user = await models.User.findByPk(req.session.userId);
await profile.denyRequest(sender, type); await user.denyRequest(sender, type);
return true; return true;
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
@ -215,8 +212,8 @@ export function resolver(req: any, res: any): any {
} }
if (sender && type) { if (sender && type) {
try { try {
const profile = new Profile(req.session.userId); const user = await models.User.findByPk(req.session.userId);
await profile.acceptRequest(sender, type); await user.acceptRequest(sender, type);
return true; return true;
} catch (err) { } catch (err) {
globals.logger.warn(err.message); globals.logger.warn(err.message);

@ -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,18 +1,10 @@
import {Sequelize} from "sequelize"; import {Sequelize} from "sequelize-typescript";
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 {Chatroom} from "./Chatroom"; import * as models from "./models";
import * as models from "./datamodels";
import {Post} from "./Post";
import {Profile} from "./Profile";
import {User} from "./User";
const config = globals.config;
const tableCreationFile = __dirname + "/../../sql/create-tables.sql";
const tableUpdateFile = __dirname + "/../../sql/update-tables.sql";
/** /**
* 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.
@ -35,7 +27,16 @@ namespace dataaccess {
export async function init(seq: Sequelize) { export async function init(seq: Sequelize) {
sequelize = seq; sequelize = seq;
try { try {
await models.datainit(sequelize); await sequelize.addModels([
models.ChatMember,
models.ChatMessage,
models.ChatRoom,
models.Friendship,
models.Post,
models.PostVote,
models.Request,
models.User,
]);
} catch (err) { } catch (err) {
globals.logger.error(err.message); globals.logger.error(err.message);
globals.logger.debug(err.stack); globals.logger.debug(err.stack);
@ -46,10 +47,10 @@ namespace dataaccess {
* Returns the user by handle. * Returns the user by handle.
* @param userHandle * @param userHandle
*/ */
export async function getUserByHandle(userHandle: string): Promise<User> { export async function getUserByHandle(userHandle: string): Promise<models.User> {
const user = await models.SqUser.findOne({where: {handle: userHandle}}); const user = await models.User.findOne({where: {handle: userHandle}});
if (user) { if (user) {
return new User(user); return user;
} else { } else {
throw new UserNotFoundError(userHandle); throw new UserNotFoundError(userHandle);
} }
@ -60,10 +61,10 @@ namespace dataaccess {
* @param email * @param email
* @param password * @param password
*/ */
export async function getUserByLogin(email: string, password: string): Promise<Profile> { export async function getUserByLogin(email: string, password: string): Promise<models.User> {
const user = await models.SqUser.findOne({where: {email, password}}); const user = await models.User.findOne({where: {email, password}});
if (user) { if (user) {
return new Profile(user); return user;
} else { } else {
throw new UserNotFoundError(email); throw new UserNotFoundError(email);
} }
@ -75,12 +76,11 @@ namespace dataaccess {
* @param email * @param email
* @param password * @param password
*/ */
export async function registerUser(username: string, email: string, password: string) { export async function registerUser(username: string, email: string, password: string): Promise<models.User> {
const existResult = !!(await models.SqUser.findOne({where: {username, email, password}})); const existResult = !!(await models.User.findOne({where: {username, email, password}}));
const handle = generateHandle(username); const handle = generateHandle(username);
if (!existResult) { if (!existResult) {
const user = await models.SqUser.create({username, email, password, handle}); return models.User.create({username, email, password, handle});
return new Profile(user);
} else { } else {
throw new EmailAlreadyRegisteredError(email); throw new EmailAlreadyRegisteredError(email);
} }
@ -90,10 +90,10 @@ namespace dataaccess {
* Returns a post for a given postId.s * Returns a post for a given postId.s
* @param postId * @param postId
*/ */
export async function getPost(postId: number): Promise<Post> { export async function getPost(postId: number): Promise<models.Post> {
const post = await models.SqPost.findByPk(postId); const post = await models.Post.findByPk(postId);
if (post) { if (post) {
return new Post(post); return post;
} else { } else {
return null; return null;
} }
@ -107,18 +107,20 @@ 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 posts = await models.SqPost.findAll({order: [["createdAt", "DESC"]], limit: first, offset}); return models.Post.findAll({
return posts.map((p) => new Post(p)); include: [{association: "rVotes"}],
limit: first,
offset,
order: [["createdAt", "DESC"]],
});
} else { } else {
const results: models.SqPost[] = await sequelize.query( return await sequelize.query(
`SELECT id FROM ( `SELECT * FROM (
SELECT *, SELECT *,
(SELECT count(*) FROM votes WHERE vote_type = 'UPVOTE' AND item_id = posts.id) AS upvotes , (SELECT count(*) FROM post_votes WHERE vote_type = 'UPVOTE' AND post_id = posts.id) AS upvotes ,
(SELECT count(*) FROM votes WHERE vote_type = 'DOWNVOTE' AND item_id = posts.id) AS downvotes (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 ?`, FROM posts) AS a ORDER BY (a.upvotes - a.downvotes) DESC LIMIT ? OFFSET ?`,
{replacements: [first, offset], mapToModel: true, model: models.SqPost}); {replacements: [first, offset], mapToModel: true, model: models.Post}) as models.Post[];
return results.map((p) => new Post(p));
} }
} }
@ -128,10 +130,9 @@ namespace dataaccess {
* @param authorId * @param authorId
* @param type * @param type
*/ */
export async function createPost(content: string, authorId: number, type?: string): Promise<Post> { export async function createPost(content: string, authorId: number, type?: string): Promise<models.Post> {
type = type || "MISC"; type = type || "MISC";
const sqPost = await models.SqPost.create({content, userId: authorId}); const post = await models.Post.create({content, authorId});
const post = new Post(sqPost);
globals.internalEmitter.emit(InternalEvents.POSTCREATE, post); globals.internalEmitter.emit(InternalEvents.POSTCREATE, post);
return post; return post;
} }
@ -141,7 +142,7 @@ namespace dataaccess {
* @param postId * @param postId
*/ */
export async function deletePost(postId: number): Promise<boolean> { export async function deletePost(postId: number): Promise<boolean> {
await (await models.SqPost.findByPk(postId)).destroy(); await (await models.Post.findByPk(postId)).destroy();
return true; return true;
} }
@ -149,15 +150,16 @@ namespace dataaccess {
* Creates a chatroom containing two users * Creates a chatroom containing two users
* @param members * @param members
*/ */
export async function createChat(...members: number[]): Promise<Chatroom> { export async function createChat(...members: number[]): Promise<models.ChatRoom> {
return sequelize.transaction(async (t) => { return sequelize.transaction(async (t) => {
const chat = await models.SqChat.create({}, {transaction: t}); const chat = await models.ChatRoom.create({}, {transaction: t, include: [models.User]});
for (const member of members) { for (const member of members) {
await chat.addMember(Number(member), {transaction: t}); const user = await models.User.findByPk(member);
await chat.$add("rMember", user, {transaction: t});
} }
const chatroom = new Chatroom(chat); await chat.save({transaction: t});
globals.internalEmitter.emit(InternalEvents.CHATCREATE, chatroom); globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat);
return chatroom; return chat;
}); });
} }
@ -168,22 +170,21 @@ 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 = await models.SqChat.findByPk(chatId); const chat = await models.ChatRoom.findByPk(chatId);
if (chat) { if (chat) {
const message = await chat.createMessage({content, userId: authorId}); const message = await chat.$create("rMessage", {content, authorId}) as models.ChatMessage;
globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message.message); globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message);
return message.message; return message;
} else { } else {
throw new ChatNotFoundError(chatId); throw new ChatNotFoundError(chatId);
} }
} }
/** /**
* Returns all chats. * Returns all rChats.
*/ */
export async function getAllChats(): Promise<Chatroom[]> { export async function getAllChats(): Promise<models.ChatRoom[]> {
const chats = await models.SqChat.findAll(); return models.ChatRoom.findAll();
return chats.map((c) => new Chatroom(c));
} }
/** /**
@ -195,7 +196,7 @@ namespace dataaccess {
export async function createRequest(sender: number, receiver: number, requestType?: RequestType) { export async function createRequest(sender: number, receiver: number, requestType?: RequestType) {
requestType = requestType || RequestType.FRIENDREQUEST; requestType = requestType || RequestType.FRIENDREQUEST;
const request = await models.SqRequest.create({senderId: sender, receiverId: receiver, requestType}); const request = await models.Request.create({senderId: sender, receiverId: receiver, requestType});
globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, Request); globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, Request);
return request; return request;
} }

@ -1,38 +0,0 @@
import markdown from "../markdown";
import {Chatroom} from "./Chatroom";
import * as models from "./datamodels/models";
import {User} from "./User";
export class ChatMessage {
public id: number;
public content: string;
public createdAt: Date;
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());
}
/**
* Returns the rendered html content of the chat message.
*/
public htmlContent(): string {
return markdown.renderInline(this.content);
}
/**
* returns the chatroom for the chatmessage.
*/
public async chat(): Promise<Chatroom> {
return (await this.message.getChat()).chatroom;
}
}

@ -1,38 +0,0 @@
import {SqChat} from "./datamodels";
import {User} from "./User";
export class Chatroom {
public readonly id: number;
public namespace: string;
constructor(private chat: SqChat) {
this.id = chat.id;
this.namespace = `/chat/${chat.id}`;
}
/**
* Returns all members of a chatroom.
*/
public async members(): Promise<User[]> {
const members = await this.chat.getMembers();
return members.map((m) => new User(m));
}
/**
* 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 messages = await this.chat.getMessages({limit: lim, offset: offs});
if (containing) {
return messages.filter((x) => x.content.includes(containing)).map((m) => m.message);
} else {
return messages.map((m) => m.message);
}
}
}

@ -1,95 +0,0 @@
import markdown from "../markdown";
import {SqPost, SqPostVotes} from "./datamodels";
import {PostVotes} from "./datamodels/models";
import dataaccess from "./index";
import {User} from "./User";
export class Post {
public readonly id: number;
public createdAt: Date;
public content: string;
public type: string;
private post: SqPost;
constructor(post: SqPost) {
this.id = post.id;
this.createdAt = post.createdAt;
this.post = post;
this.type = "";
this.content = post.content;
}
/**
* Returns the upvotes of a post.
*/
public async upvotes(): Promise<number> {
return PostVotes.count({where: {voteType: dataaccess.VoteType.UPVOTE, post_id: this.id}});
}
/**
* Returns the downvotes of the post
*/
public async downvotes(): Promise<number> {
return PostVotes.count({where: {voteType: dataaccess.VoteType.DOWNVOTE, post_id: this.id}});
}
/**
* the content rendered by markdown-it.
*/
public async htmlContent(): Promise<string> {
return markdown.render(this.content);
}
/**
* The autor of the post.
*/
public async author(): Promise<User> {
return new User(await this.post.getUser());
}
/**
* Deletes the post.
*/
public async delete(): Promise<void> {
await this.post.destroy();
}
/**
* The type of vote the user performed on the post.
*/
public async userVote(userId: number): Promise<dataaccess.VoteType> {
const votes = await this.post.getVotes({where: {userId}});
if (votes.length >= 1) {
return votes[0].voteType;
} else {
return null;
}
}
/**
* Performs a vote on a post.
* @param userId
* @param type
*/
public async vote(userId: number, type: dataaccess.VoteType): Promise<dataaccess.VoteType> {
type = type || dataaccess.VoteType.UPVOTE;
let vote = await SqPostVotes.findOne({where: {user_id: userId, post_id: this.id}});
if (!vote) {
await this.post.addVote(userId);
vote = await SqPostVotes.findOne({where: {user_id: userId, post_id: this.id}});
}
if (vote) {
if (vote.voteType === type) {
await vote.destroy();
return null;
} else {
vote.voteType = type;
await vote.save();
}
}
return vote.voteType;
}
}

@ -1,161 +0,0 @@
import {RequestNotFoundError} from "../errors/RequestNotFoundError";
import {Chatroom} from "./Chatroom";
import {SqUser} from "./datamodels";
import dataaccess from "./index";
import * as wrappers from "./wrappers";
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).
* Skips the query if the user doesn't exist.
* @param first
* @param offset
*/
public async chats({first, offset}: { first: number, offset?: number }): Promise<Chatroom[]> {
first = first || 10;
offset = offset || 0;
const result = await this.user.getChats();
if (result) {
return result.map((chat) => new Chatroom(chat));
} else {
return [];
}
}
/**
* Returns all open requests the user has send.
*/
public async sentRequests() {
return this.user.getSentRequests();
}
/**
* Returns all received requests of the user.
*/
public async receivedRequests() {
return this.user.getReceivedRequests();
}
/**
* Sets the greenpoints of a user.
* @param points
*/
public async setGreenpoints(points: number): Promise<number> {
this.user.rankpoints = points;
await this.user.save();
return this.user.rankpoints;
}
/**
* Sets the email of the user
* @param email
*/
public async setEmail(email: string): Promise<string> {
this.user.email = email;
await this.user.save();
return this.user.email;
}
/**
* Updates the handle of the user
*/
public async setHandle(handle: string): Promise<string> {
this.user.handle = handle;
await this.user.save();
return this.user.handle;
}
/**
* Sets the username of the user
* @param name
*/
public async setName(name: string): Promise<string> {
this.user.username = name;
await this.user.save();
return this.user.username;
}
/**
* Denys a request.
* @param sender
* @param type
*/
public async denyRequest(sender: number, type: dataaccess.RequestType) {
const request = await this.user.getReceivedRequests({where: {senderId: sender, requestType: type}});
if (request[0]) {
await request[0].destroy();
}
}
/**
* Accepts a request.
* @param sender
* @param type
*/
public async acceptRequest(sender: number, type: dataaccess.RequestType) {
const requests = await this.user.getReceivedRequests({where: {senderId: sender, requestType: type}});
if (requests.length > 0) {
const request = requests[0];
if (request.requestType === dataaccess.RequestType.FRIENDREQUEST) {
await this.user.addFriend(sender);
await request.destroy();
}
} else {
throw new RequestNotFoundError(sender, this.id, type);
}
}
}

@ -1,53 +0,0 @@
import {SqUser} from "./datamodels";
import * as wrappers from "./wrappers";
export class User {
public id: number;
public name: string;
public handle: string;
public greenpoints: number;
public joinedAt: Date;
protected user: SqUser;
constructor(user: SqUser) {
this.id = user.id;
this.name = user.username;
this.handle = user.handle;
this.greenpoints = user.rankpoints;
this.joinedAt = user.joinedAt;
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<User[]> {
const result = await this.user.getFriends();
const userFriends = [];
for (const friend of result) {
userFriends.push(new 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;
}
}

@ -1,12 +0,0 @@
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";

@ -1,279 +0,0 @@
// 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,5 +0,0 @@
export {User} from "./User";
export {Chatroom} from "./Chatroom";
export {Post} from "./Post";
export {Profile} from "./Profile";
export {ChatMessage} from "./ChatMessage";

@ -1,8 +1,13 @@
import {GraphQLError} from "graphql"; import {GraphQLError} from "graphql";
export class NotLoggedInGqlError extends GraphQLError { export class NotLoggedInGqlError extends GraphQLError {
constructor() { constructor() {
super("Not logged in"); super("Not logged in");
} }
} }
export class PostNotFoundGqlError extends GraphQLError {
constructor(postId: number) {
super(`Post '${postId}' not found!`);
}
}

@ -9,7 +9,6 @@ import {EventEmitter} from "events";
import * as fsx from "fs-extra"; import * as fsx from "fs-extra";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as winston from "winston"; import * as winston from "winston";
import {MemoryCache} from "./MemoryCache";
const configPath = "config.yaml"; const configPath = "config.yaml";
const defaultConfig = __dirname + "/../default-config.yaml"; const defaultConfig = __dirname + "/../default-config.yaml";
@ -28,7 +27,6 @@ if (!(fsx.pathExistsSync(configPath))) {
*/ */
namespace globals { namespace globals {
export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8")); export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8"));
export const cache = new MemoryCache(1200);
export const logger = winston.createLogger({ export const logger = winston.createLogger({
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({
@ -44,9 +42,6 @@ namespace globals {
], ],
}); });
export const internalEmitter = new EventEmitter(); export const internalEmitter = new EventEmitter();
cache.on("set", (key) => logger.debug(`Caching '${key}'.`));
cache.on("miss", (key) => logger.debug(`Cache miss for '${key}'`));
cache.on("hit", (key) => logger.debug(`Cache hit for '${key}'`));
} }
export default globals; export default globals;

@ -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,10 +1,7 @@
import {Router} from "express"; import {Router} from "express";
import {Namespace, Server} from "socket.io"; import {Namespace, Server} from "socket.io";
import dataaccess from "../lib/dataaccess"; import dataaccess from "../lib/dataaccess";
import {ChatMessage} from "../lib/dataaccess/ChatMessage"; import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/models";
import {Chatroom} from "../lib/dataaccess/Chatroom";
import {Request} from "../lib/dataaccess/datamodels/models";
import {Post} from "../lib/dataaccess/Post";
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 +34,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", Object.assign(post, {htmlContent: post.htmlContent()})); 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, async (request: Request) => { globals.internalEmitter.on(InternalEvents.REQUESTCREATE, async (request: Request) => {
if ((await request.getSender()).id === socket.handshake.session.userId) { if ((await request.$get("sender") as User).id === socket.handshake.session.userId) {
socket.emit("request", request); socket.emit("request", request);
} }
}); });
globals.internalEmitter.on(InternalEvents.GQLPOSTCREATE, async (post: Post) => { globals.internalEmitter.on(InternalEvents.GQLPOSTCREATE, async (post: Post) => {
socket.emit("post", Object.assign(post, {htmlContent: post.htmlContent()})); socket.emit("post", Object.assign(post, {htmlContent: post.htmlContent}));
}); });
}); });
@ -56,7 +53,7 @@ class HomeRoute extends Route {
for (const chat of chats) { for (const chat of chats) {
chatRooms[chat.id] = this.getChatSocketNamespace(chat.id); chatRooms[chat.id] = this.getChatSocketNamespace(chat.id);
} }
globals.internalEmitter.on(InternalEvents.CHATCREATE, (chat: Chatroom) => { globals.internalEmitter.on(InternalEvents.CHATCREATE, (chat: ChatRoom) => {
chatRooms[chat.id] = this.getChatSocketNamespace(chat.id); chatRooms[chat.id] = this.getChatSocketNamespace(chat.id);
}); });
} }
@ -82,15 +79,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", Object.assign(message, {htmlContent: message.htmlContent()})); socket.broadcast.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent}));
socket.emit("chatMessageSent", Object.assign(message, {htmlContent: message.htmlContent()})); 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, async (message: ChatMessage) => { globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, async (message: ChatMessage) => {
if ((await message.chat()).id === chatId) { if ((await message.$get("chat") as ChatRoom).id === chatId) {
socket.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent()})); socket.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent}));
} }
}); });
}); });

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

@ -10,7 +10,9 @@
"target": "es2018", "target": "es2018",
"allowJs": true, "allowJs": true,
"moduleResolution": "node", "moduleResolution": "node",
"module": "commonjs" "module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}, },
"include": [ "include": [
"src/**/*" "src/**/*"

Loading…
Cancel
Save