From 69e535276b57a08fa0a0fdbb745b3618af920292 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Mon, 23 Sep 2019 16:13:48 +0200 Subject: [PATCH 01/14] Added sessions --- package-lock.json | 64 +++++++++++++++++++++++++++++++---- package.json | 8 +++-- src/app.ts | 43 +++++++++++++++++++++++ src/default-config.yaml | 4 +++ src/lib/dataaccess/Profile.ts | 51 ++++++++++++++++++++++++++++ src/lib/dataaccess/User.ts | 47 ------------------------- src/lib/dataaccess/index.ts | 39 +++++++++++++++++++++ src/routes/index.ts | 4 +-- src/sql/create-tables.sql | 7 ++++ 9 files changed, 208 insertions(+), 59 deletions(-) create mode 100644 src/lib/dataaccess/Profile.ts diff --git a/package-lock.json b/package-lock.json index 7baeb51..99cd62f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,15 @@ "@types/node": "*" } }, + "@types/compression": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.0.1.tgz", + "integrity": "sha512-GuoIYzD70h+4JUqUabsm31FGqvpCYHGKcLtor7nQ/YvUyNX0o9SJZ9boFI5HjFfbOda5Oe/XOvNK6FES8Y/79w==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/connect": { "version": "3.4.32", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", @@ -153,7 +162,8 @@ "@types/js-yaml": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.1.tgz", - "integrity": "sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==" + "integrity": "sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==", + "dev": true }, "@types/mime": { "version": "2.0.1", @@ -215,6 +225,7 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.4.4.tgz", "integrity": "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==", + "dev": true, "requires": { "winston": "*" } @@ -1310,6 +1321,40 @@ "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, + "compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "requires": { + "mime-db": ">= 1.40.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1361,9 +1406,9 @@ } }, "connect-pg-simple": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-6.0.0.tgz", - "integrity": "sha512-6pQnRSGFyswyHMdKQp5C+g78fjU/1/6eY05VeixXwMixw5KYhAcoOCXyf8TdPE1IzRLNDBMQi64vojXK/HMXVw==", + "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" } @@ -2691,12 +2736,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2715,6 +2762,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2894,7 +2942,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3000,7 +3049,8 @@ "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/package.json b/package.json index 0fb8448..664e5a3 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "author": "SoftEngI", "license": "ISC", "devDependencies": { + "@types/compression": "^1.0.1", "@types/connect-pg-simple": "^4.2.0", "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.1", @@ -26,9 +27,11 @@ "@types/express-socket.io-session": "^1.3.2", "@types/fs-extra": "^8.0.0", "@types/graphql": "^14.2.3", + "@types/js-yaml": "^3.12.1", "@types/node": "^12.7.2", "@types/pg": "^7.11.0", "@types/socket.io": "^2.1.2", + "@types/winston": "^2.4.4", "delete": "^1.1.0", "gulp": "^4.0.2", "gulp-minify": "^3.1.0", @@ -40,9 +43,8 @@ "typescript": "^3.5.3" }, "dependencies": { - "@types/js-yaml": "^3.12.1", - "@types/winston": "^2.4.4", - "connect-pg-simple": "^6.0.0", + "compression": "^1.7.4", + "connect-pg-simple": "^6.0.1", "cookie-parser": "^1.4.4", "express": "^4.17.1", "express-graphql": "^0.9.0", diff --git a/src/app.ts b/src/app.ts index c7079ac..6758970 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,12 @@ +import * as compression from "compression"; +import connectPgSimple = require("connect-pg-simple"); +import * as cookieParser from "cookie-parser"; import * as express from "express"; +import * as graphqlHTTP from "express-graphql"; +import * as session from "express-session"; +import sharedsession = require("express-socket.io-session"); +import {buildSchema} from "graphql"; +import {importSchema} from "graphql-import"; import * as http from "http"; import * as path from "path"; import * as socketIo from "socket.io"; @@ -6,6 +14,8 @@ import dataaccess from "./lib/dataaccess"; import globals from "./lib/globals"; import routes from "./routes"; +const PgSession = connectPgSimple(session); + class App { public app: express.Application; public io: socketIo.Server; @@ -23,10 +33,43 @@ class App { public async init() { await dataaccess.init(); await routes.ioListeners(this.io); + + const appSession = session({ + cookie: { + maxAge: Number(globals.config.session.cookieMaxAge), + secure: "auto", + }, + resave: false, + saveUninitialized: true, // TODO: Set to false and only save when accepted by user. + secret: globals.config.session.secret, + store: new PgSession({ + pool: dataaccess.pool, + tableName: "user_sessions", + }), + }); + + this.io.use(sharedsession(appSession, {autoSave: true})); + this.app.set("views", path.join(__dirname, "views")); this.app.set("view engine", "pug"); + this.app.set("trust proxy", 1); + + this.app.use(compression()); + this.app.use(express.json()); + this.app.use(express.urlencoded({extended: false})); this.app.use(express.static(path.join(__dirname, "public"))); + this.app.use(cookieParser()); + this.app.use(appSession); this.app.use(routes.router); + this.app.use("/graphql", graphqlHTTP(async (request, response) => { + return { + // @ts-ignore all + context: {session: request.session}, + graphiql: true, + rootValue: await routes.resolvers(request, response), + schema: buildSchema(importSchema("./public/graphql/schema.graphql")), + }; + })); } /** diff --git a/src/default-config.yaml b/src/default-config.yaml index 417ccda..e232512 100644 --- a/src/default-config.yaml +++ b/src/default-config.yaml @@ -9,3 +9,7 @@ database: # http server configuration server: port: 8080 + +session: + secret: REPLACE WITH SAFE RANDOM GENERATED SECRET + cookieMaxAge: 604800000‬ # 7 days diff --git a/src/lib/dataaccess/Profile.ts b/src/lib/dataaccess/Profile.ts new file mode 100644 index 0000000..a3f3a46 --- /dev/null +++ b/src/lib/dataaccess/Profile.ts @@ -0,0 +1,51 @@ +import {queryHelper} from "./index"; +import {User} from "./User"; + +export class Profile extends User { + /** + * Sets the greenpoints of a user. + * @param points + */ + public async setGreenpoints(points: number): Promise { + 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 { + const result = await queryHelper.first({ + text: "UPDATE TABLE 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 { + const result = await queryHelper.first({ + text: "UPDATE TABLE 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 { + const result = await queryHelper.first({ + text: "UPDATE TABLE users SET name = $1 WHERE id = $2", + values: [name, this.id], + }); + return result.name; + } +} diff --git a/src/lib/dataaccess/User.ts b/src/lib/dataaccess/User.ts index bb680c4..394c062 100644 --- a/src/lib/dataaccess/User.ts +++ b/src/lib/dataaccess/User.ts @@ -17,18 +17,6 @@ export class User extends DataObject { return this.$name; } - /** - * Sets the username of the user - * @param name - */ - public async setName(name: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE TABLE users SET name = $1 WHERE id = $2", - values: [name, this.id], - }); - return result.name; - } - /** * The unique handle of the user. */ @@ -37,17 +25,6 @@ export class User extends DataObject { return this.$handle; } - /** - * Updates the handle of the user - */ - public async setHandle(handle: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE TABLE users SET handle = $1 WHERE id = $2", - values: [handle, this.id], - }); - return result.handle; - } - /** * The email of the user */ @@ -56,18 +33,6 @@ export class User extends DataObject { return this.$email; } - /** - * Sets the email of the user - * @param email - */ - public async setEmail(email: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE TABLE users SET email = $1 WHERE users.id = $2 RETURNING email", - values: [email, this.id], - }); - return result.email; - } - /** * The number of greenpoints of the user */ @@ -76,18 +41,6 @@ export class User extends DataObject { return this.$greenpoints; } - /** - * Sets the greenpoints of a user. - * @param points - */ - public async setGreenpoints(points: number): Promise { - const result = await queryHelper.first({ - text: "UPDATE users SET greenpoints = $1 WHERE id = $2 RETURNING greenpoints", - values: [points, this.id], - }); - return result.greenpoints; - } - /** * The date the user joined the platform */ diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 1d610ef..5696547 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -1,6 +1,7 @@ import {Pool} from "pg"; import globals from "../globals"; import {QueryHelper} from "../QueryHelper"; +import {Profile} from "./Profile"; import {User} from "./User"; const config = globals.config; @@ -16,7 +17,18 @@ const dbClient: Pool = new Pool({ }); 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 dataaccess { + + export const pool: Pool = dbClient; + /** * Initializes everything that needs to be initialized asynchronous. */ @@ -45,6 +57,33 @@ namespace dataaccess { return new User(result.id, result); } + /** + * Returns the user by email and password + * @param email + * @param password + */ + export async function getUserByLogin(email: string, password: string) { + const result = await this.queryHelper.first({ + text: "SELECT * FROM users WHERE email = $1 AND password = $2", + values: [email, password], + }); + return new Profile(result.id, result); + } + + /** + * 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 result = await this.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); + } + /** * Enum representing the types of votes that can be performed on a post. */ diff --git a/src/routes/index.ts b/src/routes/index.ts index 84cc7c5..e512ee6 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -28,9 +28,9 @@ namespace routes { * @param request * @param response */ - export const resolvers = async (request: any, response: any): Promise => { + export async function resolvers(request: any, response: any): Promise { return homeRoute.resolver(request, response); - }; + } /** * Assigns the io listeners or namespaces to the routes diff --git a/src/sql/create-tables.sql b/src/sql/create-tables.sql index 7c1c97a..faf2137 100644 --- a/src/sql/create-tables.sql +++ b/src/sql/create-tables.sql @@ -1,3 +1,10 @@ +CREATE TABLE IF NOT EXISTS "user_sessions" ( + "sid" varchar NOT NULL COLLATE "default", + "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, From 1b9e7e1a66d22714e2c918f45c7e8726e30d3f65 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Mon, 23 Sep 2019 16:16:40 +0200 Subject: [PATCH 02/14] Fixed maxAge value of config --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 6758970..3a60de6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -36,7 +36,7 @@ class App { const appSession = session({ cookie: { - maxAge: Number(globals.config.session.cookieMaxAge), + maxAge: Number(globals.config.session.cookieMaxAge) || 604800000, secure: "auto", }, resave: false, From 6121aff29f8429fe35be315d292b2708c13575f9 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Mon, 23 Sep 2019 21:18:09 +0200 Subject: [PATCH 03/14] Added chat data objects --- package-lock.json | 41 +++++------------- src/lib/dataaccess/ChatMessage.ts | 7 ++++ src/lib/dataaccess/Chatroom.ts | 51 +++++++++++++++++++++++ src/public/graphql/schema.graphql | 3 ++ src/views/home/index.pug | 2 +- src/views/{home => includes}/stylebar.pug | 0 src/views/login/index.pug | 2 +- src/views/login/stylebar.pug | 2 - src/views/register/index.pug | 2 +- src/views/register/stylebar.pug | 2 - 10 files changed, 75 insertions(+), 37 deletions(-) create mode 100644 src/lib/dataaccess/ChatMessage.ts create mode 100644 src/lib/dataaccess/Chatroom.ts rename src/views/{home => includes}/stylebar.pug (100%) delete mode 100644 src/views/login/stylebar.pug delete mode 100644 src/views/register/stylebar.pug diff --git a/package-lock.json b/package-lock.json index 99cd62f..c82d4b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2526,8 +2526,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -2548,14 +2547,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2570,20 +2567,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -2700,8 +2694,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -2713,7 +2706,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2728,7 +2720,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2736,14 +2727,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2762,7 +2751,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2843,8 +2831,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -2856,7 +2843,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -2942,8 +2928,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -2979,7 +2964,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2999,7 +2983,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3043,14 +3026,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, diff --git a/src/lib/dataaccess/ChatMessage.ts b/src/lib/dataaccess/ChatMessage.ts new file mode 100644 index 0000000..5930679 --- /dev/null +++ b/src/lib/dataaccess/ChatMessage.ts @@ -0,0 +1,7 @@ +import {Chatroom} from "./Chatroom"; +import {User} from "./User"; + +export class ChatMessage { + constructor(public author: User, public chat: Chatroom, public timestamp: number, public content: string) { + } +} diff --git a/src/lib/dataaccess/Chatroom.ts b/src/lib/dataaccess/Chatroom.ts new file mode 100644 index 0000000..c6fe144 --- /dev/null +++ b/src/lib/dataaccess/Chatroom.ts @@ -0,0 +1,51 @@ +import {ChatMessage} from "./ChatMessage"; +import {queryHelper} from "./index"; +import {User} from "./User"; + +export class Chatroom { + + constructor(private id: number) {} + + /** + * Returns all members of a chatroom. + */ + public async members(): Promise { + const result = await queryHelper.all({ + 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) { + chatMembers.push(new User(row)); + } + 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(limit?: number, offset?: number, containing?: string) { + const lim = limit || 16; + const offs = offset || 0; + + const result = await queryHelper.all({ + text: "SELECT * FROM chat_messages WHERE chat = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", + values: [this.id, lim, offs], + }); + + const messages = []; + for (const row of result) { + messages.push(new ChatMessage(new User(row.author), this, row.timestamp, row.content)); + } + if (containing) { + return messages.filter((x) => x.content.includes(containing)); + } else { + return messages; + } + } +} diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index ff60fd3..c5b03c3 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -19,6 +19,9 @@ type Query { } type Mutation { + "Accepts the usage of cookies." + acceptCookies: Boolean + "Upvote/downvote a Post" vote(postId: ID!, type: [VoteType!]!): Boolean diff --git a/src/views/home/index.pug b/src/views/home/index.pug index 0fc45ad..7da0198 100644 --- a/src/views/home/index.pug +++ b/src/views/home/index.pug @@ -4,6 +4,6 @@ html include ../includes/head body div#content - include stylebar + include ../includes/stylebar include feed include friends diff --git a/src/views/home/stylebar.pug b/src/views/includes/stylebar.pug similarity index 100% rename from src/views/home/stylebar.pug rename to src/views/includes/stylebar.pug diff --git a/src/views/login/index.pug b/src/views/login/index.pug index 41fe877..81c7585 100644 --- a/src/views/login/index.pug +++ b/src/views/login/index.pug @@ -4,5 +4,5 @@ html include ../includes/head body div#content - include stylebar + include ../includes/stylebar include login diff --git a/src/views/login/stylebar.pug b/src/views/login/stylebar.pug deleted file mode 100644 index c6c36e3..0000000 --- a/src/views/login/stylebar.pug +++ /dev/null @@ -1,2 +0,0 @@ -div.stylebar - h1 Greenvironment diff --git a/src/views/register/index.pug b/src/views/register/index.pug index d8e9dd1..7ade6a7 100644 --- a/src/views/register/index.pug +++ b/src/views/register/index.pug @@ -4,5 +4,5 @@ html include ../includes/head body div#content - include stylebar + include ../includes/stylebar include register diff --git a/src/views/register/stylebar.pug b/src/views/register/stylebar.pug deleted file mode 100644 index c6c36e3..0000000 --- a/src/views/register/stylebar.pug +++ /dev/null @@ -1,2 +0,0 @@ -div.stylebar - h1 Greenvironment From e6d219126683dad9df74e92971e36d8746f45c89 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 24 Sep 2019 14:20:40 +0200 Subject: [PATCH 04/14] Fixed graphql - fixed graphql resolution - added login, logout, acceptCookies implementation --- package-lock.json | 11 ++++++++++ package.json | 2 ++ src/app.ts | 20 +++++++++++------- src/lib/dataaccess/index.ts | 14 ++++++++----- src/lib/globals.ts | 7 ++++++- src/public/graphql/schema.graphql | 6 ++++++ src/routes/home.ts | 35 +++++++++++++++++++++++++++++-- src/routes/index.ts | 2 +- 8 files changed, 81 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index c82d4b5..3689e96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -159,6 +159,12 @@ "integrity": "sha512-UoCovaxbJIxagCvVfalfK7YaNhmxj3BQFRQ2RHQKLiu+9wNXhJnlbspsLHt/YQM99IaLUUFJNzCwzc6W0ypMeQ==", "dev": true }, + "@types/http-status": { + "version": "0.2.30", + "resolved": "https://registry.npmjs.org/@types/http-status/-/http-status-0.2.30.tgz", + "integrity": "sha512-wcBc5XEOMmhuoWfNhwnpw8+tVAsueUeARxCTcRQ0BCN5V/dyKQBJNWdxmvcZW5IJWoeU47UWQ+ACCg48KKnqyA==", + "dev": true + }, "@types/js-yaml": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.1.tgz", @@ -3664,6 +3670,11 @@ "sshpk": "^1.7.0" } }, + "http-status": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.3.2.tgz", + "integrity": "sha512-vR1YTaDyi2BukI0UiH01xy92oiZi4in7r0dmSPnrZg72Vu1SzyOLalwWP5NUk1rNiB2L+XVK2lcSVOqaertX8A==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/package.json b/package.json index 664e5a3..7dcee4a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@types/express-socket.io-session": "^1.3.2", "@types/fs-extra": "^8.0.0", "@types/graphql": "^14.2.3", + "@types/http-status": "^0.2.30", "@types/js-yaml": "^3.12.1", "@types/node": "^12.7.2", "@types/pg": "^7.11.0", @@ -54,6 +55,7 @@ "g": "^2.0.1", "graphql": "^14.4.2", "graphql-import": "^0.7.1", + "http-status": "^1.3.2", "js-yaml": "^3.13.1", "pg": "^7.12.1", "pug": "^2.0.4", diff --git a/src/app.ts b/src/app.ts index 3a60de6..7ea9b9b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -14,6 +14,8 @@ import dataaccess from "./lib/dataaccess"; import globals from "./lib/globals"; import routes from "./routes"; +const logger = globals.logger; + const PgSession = connectPgSimple(session); class App { @@ -40,7 +42,7 @@ class App { secure: "auto", }, resave: false, - saveUninitialized: true, // TODO: Set to false and only save when accepted by user. + saveUninitialized: false, secret: globals.config.session.secret, store: new PgSession({ pool: dataaccess.pool, @@ -60,14 +62,18 @@ class App { this.app.use(express.static(path.join(__dirname, "public"))); this.app.use(cookieParser()); this.app.use(appSession); + this.app.use((req, res, next) => { + logger.verbose(`${req.method} ${req.url}`); + next(); + }); this.app.use(routes.router); - this.app.use("/graphql", graphqlHTTP(async (request, response) => { + this.app.use("/graphql", graphqlHTTP((request, response) => { return { // @ts-ignore all context: {session: request.session}, graphiql: true, - rootValue: await routes.resolvers(request, response), - schema: buildSchema(importSchema("./public/graphql/schema.graphql")), + rootValue: routes.resolvers(request, response), + schema: buildSchema(importSchema(path.join(__dirname, "./public/graphql/schema.graphql"))), }; })); } @@ -77,11 +83,11 @@ class App { */ public start() { if (globals.config.server.port) { - globals.logger.info(`Starting server...`); + logger.info(`Starting server...`); this.app.listen(globals.config.server.port); - globals.logger.info(`Server running on port ${globals.config.server.port}`); + logger.info(`Server running on port ${globals.config.server.port}`); } else { - globals.logger.error("No port specified in the config." + + logger.error("No port specified in the config." + "Please configure a port in the config.yaml."); } } diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 5696547..6a9c854 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -50,7 +50,7 @@ namespace dataaccess { * @param userHandle */ export async function getUserByHandle(userHandle: string) { - const result = await this.queryHelper.first({ + const result = await queryHelper.first({ text: "SELECT * FROM users WHERE users.handle = $1", values: [userHandle], }); @@ -62,12 +62,16 @@ namespace dataaccess { * @param email * @param password */ - export async function getUserByLogin(email: string, password: string) { - const result = await this.queryHelper.first({ + export async function getUserByLogin(email: string, password: string): Promise { + const result = await queryHelper.first({ text: "SELECT * FROM users WHERE email = $1 AND password = $2", values: [email, password], }); - return new Profile(result.id, result); + if (result) { + return new Profile(result.id, result); + } else { + return null; + } } /** @@ -77,7 +81,7 @@ namespace dataaccess { * @param password */ export async function registerUser(username: string, email: string, password: string) { - const result = await this.queryHelper.first({ + 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], }); diff --git a/src/lib/globals.ts b/src/lib/globals.ts index a85ffea..3457fa5 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -15,6 +15,10 @@ const defaultConfig = __dirname + "/../default-config.yaml"; // ensure that the config exists by copying the default config. if (!(fsx.pathExistsSync(configPath))) { fsx.copySync(defaultConfig, configPath); +} else { + const conf = yaml.safeLoad(fsx.readFileSync(configPath, "utf-8")); + const defConf = yaml.safeLoad(fsx.readFileSync(defaultConfig, "utf-8")); + fsx.writeFileSync(configPath, yaml.safeDump(Object.assign(defConf, conf))); } /** @@ -28,10 +32,11 @@ namespace globals { format: winston.format.combine( winston.format.timestamp(), winston.format.colorize(), - winston.format.printf(({ level, message, label, timestamp }) => { + winston.format.printf(({ level, message, timestamp }) => { return `${timestamp} ${level}: ${message}`; }), ), + level: "debug", }), ], }); diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index c5b03c3..ea9c4eb 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -22,6 +22,12 @@ type Mutation { "Accepts the usage of cookies." acceptCookies: Boolean + "Login of the user. The passwordHash should be a sha512 hash of the password." + login(email: String, passwordHash: String): User + + "Logout of the user." + logout: Boolean + "Upvote/downvote a Post" vote(postId: ID!, type: [VoteType!]!): Boolean diff --git a/src/routes/home.ts b/src/routes/home.ts index 13bb254..2af5947 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -1,5 +1,9 @@ import {Router} from "express"; +import {GraphQLError} from "graphql"; +import * as status from "http-status"; +import {constants} from "http2"; import {Server} from "socket.io"; +import dataaccess from "../lib/dataaccess"; import Route from "../lib/Route"; /** @@ -36,9 +40,36 @@ class HomeRoute extends Route { * @param req - the request object * @param res - the response object */ - public async resolver(req: any, res: any): Promise { + public resolver(req: any, res: any): any { return { - // TODO: Define grapql resolvers + acceptCookies() { + req.session.cookiesAccepted = true; + return true; + }, + async login(args: any) { + if (args.email && args.passwordHash) { + const user = await dataaccess.getUserByLogin(args.email, args.passwordHash); + if (user && user.id) { + req.session.userId = user.id; + return user; + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("Invalid login data."); + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No email or password given."); + } + }, + logout() { + if (req.session.user) { + delete req.session.user; + return true; + } else { + res.status(status.UNAUTHORIZED); + return new GraphQLError("User is not logged in."); + } + }, }; } diff --git a/src/routes/index.ts b/src/routes/index.ts index e512ee6..eb65750 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -28,7 +28,7 @@ namespace routes { * @param request * @param response */ - export async function resolvers(request: any, response: any): Promise { + export function resolvers(request: any, response: any): Promise { return homeRoute.resolver(request, response); } From 1d97e3305efc8a87dd815b8d267235d16e852aca Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 24 Sep 2019 18:06:04 +0200 Subject: [PATCH 05/14] API implementation --- package-lock.json | 41 +++++++++++++++++------- src/lib/QueryHelper.ts | 2 +- src/lib/dataaccess/DataObject.ts | 6 ++-- src/lib/dataaccess/Post.ts | 52 ++++++++++++++++++++++++------- src/lib/dataaccess/User.ts | 31 ++++++++++++------ src/lib/dataaccess/index.ts | 15 +++++++++ src/public/graphql/schema.graphql | 19 ++++++----- src/routes/home.ts | 38 ++++++++++++++++++++++ src/sql/create-tables.sql | 2 +- src/sql/update-tables.sql | 9 ++++-- 10 files changed, 169 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3689e96..64e04b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2532,7 +2532,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2553,12 +2554,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2573,17 +2576,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2700,7 +2706,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2712,6 +2719,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2726,6 +2734,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2733,12 +2742,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2757,6 +2768,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2837,7 +2849,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2849,6 +2862,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2934,7 +2948,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2970,6 +2985,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2989,6 +3005,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3032,12 +3049,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/src/lib/QueryHelper.ts b/src/lib/QueryHelper.ts index 5970f70..95b7a55 100644 --- a/src/lib/QueryHelper.ts +++ b/src/lib/QueryHelper.ts @@ -133,7 +133,7 @@ export class QueryHelper { try { return await this.pool.query(query); } catch (err) { - logger.debug(`Error on query "${query}".`); + logger.debug(`Error on query "${JSON.stringify(query)}".`); logger.error(`Sql query failed: ${err}`); logger.verbose(err.stack); return { diff --git a/src/lib/dataaccess/DataObject.ts b/src/lib/dataaccess/DataObject.ts index 1890425..019bcc3 100644 --- a/src/lib/dataaccess/DataObject.ts +++ b/src/lib/dataaccess/DataObject.ts @@ -12,9 +12,9 @@ export abstract class DataObject { /** * Loads data from the database if data has not been loaded */ - protected loadDataIfNotExists() { - if (this.dataLoaded) { - this.loadData(); + protected async loadDataIfNotExists() { + if (!this.dataLoaded) { + await this.loadData(); } } } diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts index b4193c4..08f9c4b 100644 --- a/src/lib/dataaccess/Post.ts +++ b/src/lib/dataaccess/Post.ts @@ -5,8 +5,6 @@ import {User} from "./User"; export class Post extends DataObject { public readonly id: number; - private $upvotes: number; - private $downvotes: number; private $createdAt: string; private $content: string; private $author: number; @@ -16,23 +14,29 @@ export class Post extends DataObject { * Returns the upvotes of a post. */ public async upvotes(): Promise { - this.loadDataIfNotExists(); - return this.$upvotes; + const result = await queryHelper.first({ + text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'UPVOTE'", + values: [this.id], + }); + return result.count; } /** * Returns the downvotes of the post */ public async downvotes(): Promise { - this.loadDataIfNotExists(); - return this.$downvotes; + const result = await queryHelper.first({ + text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'DOWNVOTE'", + values: [this.id], + }); + return result.count; } /** * The content of the post (markdown) */ public async content(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$content; } @@ -40,7 +44,7 @@ export class Post extends DataObject { * The date the post was created at. */ public async createdAt(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$createdAt; } @@ -48,7 +52,7 @@ export class Post extends DataObject { * The autor of the post. */ public async author(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return new User(this.$author); } @@ -77,6 +81,34 @@ export class Post extends DataObject { } } + /** + * Performs a vote on a post. + * @param userId + * @param type + */ + public async vote(userId: number, type: dataaccess.VoteType): Promise { + const uVote = await this.userVote(userId); + if (uVote === type) { + await queryHelper.first({ + text: "DELETE FROM votes WHERE item_id = $1 AND user_id = $2", + values: [this.id, userId], + }); + } else { + if (uVote) { + await queryHelper.first({ + text: "UPDATE votes SET vote_type = $1 WHERE user_id = $1 AND item_id = $3", + values: [type, userId, this.id], + }); + } else { + await queryHelper.first({ + text: "INSERT INTO votes (user_id, item_id, vote_type) values ($1, $2, $3)", + values: [userId, this.id, type], + }); + } + return type; + } + } + /** * Loads the data from the database if needed. */ @@ -93,8 +125,6 @@ export class Post extends DataObject { if (result) { this.$author = result.author; this.$content = result.content; - this.$downvotes = result.downvotes; - this.$upvotes = result.upvotes; this.$createdAt = result.created_at; this.$type = result.type; this.dataLoaded = true; diff --git a/src/lib/dataaccess/User.ts b/src/lib/dataaccess/User.ts index 394c062..de5b25e 100644 --- a/src/lib/dataaccess/User.ts +++ b/src/lib/dataaccess/User.ts @@ -13,7 +13,7 @@ export class User extends DataObject { * The name of the user */ public async name(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$name; } @@ -21,7 +21,7 @@ export class User extends DataObject { * The unique handle of the user. */ public async handle(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$handle; } @@ -29,7 +29,7 @@ export class User extends DataObject { * The email of the user */ public async email(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$email; } @@ -37,25 +37,38 @@ export class User extends DataObject { * The number of greenpoints of the user */ public async greenpoints(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return this.$greenpoints; } + /** + * Returns the number of posts the user created + */ + public async numberOfPosts(): Promise { + const result = await queryHelper.first({ + text: "SELECT COUNT(*) count FROM posts WHERE author = $1", + values: [this.id], + }); + return result.count; + } + /** * The date the user joined the platform */ public async joinedAt(): Promise { - this.loadDataIfNotExists(); + await this.loadDataIfNotExists(); return new Date(this.$joinedAt); } /** * Returns all posts for a user. */ - public async posts(): Promise { + public async posts({first, offset}: {first: number, offset: number}): Promise { + first = first || 10; + offset = offset || 0; const result = await queryHelper.all({ - text: "SELECT * FROM posts WHERE author = $1", - values: [this.id], + text: "SELECT * FROM posts WHERE author = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", + values: [this.id, first, offset], }); const posts = []; @@ -74,7 +87,7 @@ export class User extends DataObject { result = this.row; } else { result = await queryHelper.first({ - text: "SELECT * FROM users WHERE user.id = $1", + text: "SELECT * FROM users WHERE users.id = $1", values: [this.id], }); } diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 6a9c854..a0e38f5 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -1,6 +1,7 @@ import {Pool} from "pg"; import globals from "../globals"; import {QueryHelper} from "../QueryHelper"; +import {Post} from "./Post"; import {Profile} from "./Profile"; import {User} from "./User"; @@ -88,6 +89,20 @@ namespace dataaccess { return new Profile(result.id, result); } + /** + * Creates a post + * @param content + * @param authorId + * @param type + */ + export async function createPost(content: string, authorId: number, type: string) { + const result = await queryHelper.first({ + text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *", + values: [content, authorId, type], + }); + return new Post(result.id, result); + } + /** * Enum representing the types of votes that can be performed on a post. */ diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index ea9c4eb..eba344a 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -2,6 +2,9 @@ type Query { "returns the user object for a given user id" getUser(userId: ID): User + "returns the logged in user" + getSelf: User + "returns the post object for a post id" getPost(postId: ID): Post @@ -25,11 +28,14 @@ type Mutation { "Login of the user. The passwordHash should be a sha512 hash of the password." login(email: String, passwordHash: String): User + "Registers the user." + register(username: String, email: String, passwordHash: String): User + "Logout of the user." logout: Boolean "Upvote/downvote a Post" - vote(postId: ID!, type: [VoteType!]!): Boolean + vote(postId: ID!, type: VoteType!): VoteType "Report the post" report(postId: ID!): Boolean @@ -47,7 +53,7 @@ type Mutation { sendMessage(chatId: ID!, content: String!): Boolean "create the post" - createPost(text: String, picture: String, tags: [String]): Boolean + createPost(content: String!): Boolean "delete the post for a given post id" deletePost(postId: ID!): Boolean @@ -56,7 +62,7 @@ type Mutation { "represents a single user account" type User { "url for the Profile picture of the User" - profilePicture: String! + profilePicture: String "name of the User" name: String! @@ -71,13 +77,10 @@ type User { numberOfPosts: Int "returns a given number of posts of a user" - getAllPosts(first: Int=10, offset: Int): [Post] + posts(first: Int=10, offset: Int): [Post] "creation date of the user account" - joinedDate: String! - - "returns chats the user pinned" - pinnedChats: [ChatRoom] + joinedAt: String! "returns all friends of the user" friends: [User] diff --git a/src/routes/home.ts b/src/routes/home.ts index 2af5947..1ed5115 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -4,6 +4,8 @@ import * as status from "http-status"; import {constants} from "http2"; import {Server} from "socket.io"; import dataaccess from "../lib/dataaccess"; +import {Post} from "../lib/dataaccess/Post"; +import {Profile} from "../lib/dataaccess/Profile"; import Route from "../lib/Route"; /** @@ -42,6 +44,14 @@ class HomeRoute extends Route { */ public resolver(req: any, res: any): any { return { + getSelf() { + if (req.session.userId) { + return new Profile(req.session.userId); + } else { + res.status(status.UNAUTHORIZED); + return new GraphQLError("Not logged in"); + } + }, acceptCookies() { req.session.cookiesAccepted = true; return true; @@ -70,6 +80,34 @@ class HomeRoute extends Route { return new GraphQLError("User is not logged in."); } }, + async register(args: any) { + if (args.username && args.email && args.passwordHash) { + const user = await dataaccess.registerUser(args.username, args.email, args.passwordHash); + if (user) { + req.session.userId = user.id; + return user; + } else { + res.status(status.INTERNAL_SERVER_ERROR); + return new GraphQLError("Failed to create account."); + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No username, email or password given."); + } + }, + async vote(args: any) { + if (args.postId && args.type) { + if (req.session.userId) { + return await (new Post(args.postId)).vote(req.session.userId, args.type); + } else { + res.status(status.UNAUTHORIZED); + return new GraphQLError("Not logged in."); + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No postId or type given."); + } + }, }; } diff --git a/src/sql/create-tables.sql b/src/sql/create-tables.sql index faf2137..17f6e74 100644 --- a/src/sql/create-tables.sql +++ b/src/sql/create-tables.sql @@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS posts ( created_at TIMESTAMP DEFAULT now(), content text, author SERIAL REFERENCES users (id) ON DELETE CASCADE, - type varchar(16) NOT NULL + type varchar(16) NOT NULL DEFAULT 'MISC' ); CREATE TABLE IF NOT EXISTS votes ( diff --git a/src/sql/update-tables.sql b/src/sql/update-tables.sql index 52ce2f6..13bc681 100644 --- a/src/sql/update-tables.sql +++ b/src/sql/update-tables.sql @@ -1,3 +1,8 @@ ALTER TABLE IF EXISTS votes - ADD COLUMN IF NOT EXISTS vote_type varchar(8) DEFAULT 'upvote', - ALTER COLUMN vote_type SET DEFAULT 'upvote'; + ADD COLUMN IF NOT EXISTS vote_type varchar(8) DEFAULT 'UPVOTE', + ALTER COLUMN vote_type SET DEFAULT 'UPVOTE'; + +ALTER TABLE IF EXISTS posts + ALTER COLUMN type SET DEFAULT 'MISC', + DROP COLUMN IF EXISTS upvotes, + DROP COLUMN IF EXISTS downvotes; From 84b683db11e8b98dd97c509279605c6d03beb241 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 24 Sep 2019 18:54:50 +0200 Subject: [PATCH 06/14] Removed frontend part and added graphql implementation --- gulpfile.js | 2 +- package-lock.json | 6 ++++ package.json | 1 + src/lib/dataaccess/index.ts | 14 ++++++++- src/public/graphql/schema.graphql | 7 +---- src/public/javascripts/main.js | 0 src/routes/home.ts | 49 ++++++++++++++++++------------- src/views/home/feed.pug | 13 -------- src/views/home/friends.pug | 1 - src/views/home/index.pug | 9 ------ src/views/includes/head.pug | 1 - src/views/includes/stylebar.pug | 2 -- src/views/login/index.pug | 8 ----- src/views/login/login.pug | 6 ---- src/views/register/index.pug | 8 ----- src/views/register/register.pug | 8 ----- 16 files changed, 51 insertions(+), 84 deletions(-) delete mode 100644 src/public/javascripts/main.js delete mode 100644 src/views/home/feed.pug delete mode 100644 src/views/home/friends.pug delete mode 100644 src/views/home/index.pug delete mode 100644 src/views/includes/head.pug delete mode 100644 src/views/includes/stylebar.pug delete mode 100644 src/views/login/index.pug delete mode 100644 src/views/login/login.pug delete mode 100644 src/views/register/index.pug delete mode 100644 src/views/register/register.pug diff --git a/gulpfile.js b/gulpfile.js index c1abd43..a5853d1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -3,7 +3,7 @@ const sass = require('gulp-sass'); const ts = require('gulp-typescript'); const minify = require('gulp-minify'); const del = require('delete'); - +const gulp = require('gulp'); function clearDist(cb) { del('dist/*', cb); diff --git a/package-lock.json b/package-lock.json index 64e04b1..34608d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3348,6 +3348,12 @@ } } }, + "gulp-angular": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/gulp-angular/-/gulp-angular-0.1.2.tgz", + "integrity": "sha1-ljV2ul7qoDZqMf6l7S7AHvC0O3g=", + "dev": true + }, "gulp-minify": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/gulp-minify/-/gulp-minify-3.1.0.tgz", diff --git a/package.json b/package.json index 7dcee4a..1d42af9 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@types/winston": "^2.4.4", "delete": "^1.1.0", "gulp": "^4.0.2", + "gulp-angular": "^0.1.2", "gulp-minify": "^3.1.0", "gulp-sass": "^4.0.2", "gulp-typescript": "^5.0.1", diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index a0e38f5..c0816e6 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -95,7 +95,7 @@ namespace dataaccess { * @param authorId * @param type */ - export async function createPost(content: string, authorId: number, type: string) { + export async function createPost(content: string, authorId: number, type?: string) { const result = await queryHelper.first({ text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *", values: [content, authorId, type], @@ -103,6 +103,18 @@ namespace dataaccess { return new Post(result.id, result); } + /** + * Deletes a post + * @param postId + */ + export async function deletePost(postId: number) { + const result = await queryHelper.first({ + text: "DELETE FROM posts WHERE posts.id = $1", + values: [postId], + }); + return true; + } + /** * Enum representing the types of votes that can be performed on a post. */ diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index eba344a..bb369f5 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -91,11 +91,9 @@ type User { "represents a single user post" type Post { - "returns the path to the posts picture if it has one" - picture: String "returns the text of the post" - text: String + content: String "upvotes of the Post" upvotes: Int! @@ -111,9 +109,6 @@ type Post { "returns the type of vote the user performed on the post" userVote: VoteType - - "returns the tags of the post" - tags: [String] } "represents a request of any type" diff --git a/src/public/javascripts/main.js b/src/public/javascripts/main.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/home.ts b/src/routes/home.ts index 1ed5115..2a88931 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -1,7 +1,6 @@ import {Router} from "express"; import {GraphQLError} from "graphql"; import * as status from "http-status"; -import {constants} from "http2"; import {Server} from "socket.io"; import dataaccess from "../lib/dataaccess"; import {Post} from "../lib/dataaccess/Post"; @@ -18,7 +17,6 @@ class HomeRoute extends Route { constructor() { super(); this.router = Router(); - this.configure(); } /** @@ -56,7 +54,7 @@ class HomeRoute extends Route { req.session.cookiesAccepted = true; return true; }, - async login(args: any) { + async login(args: {email: string, passwordHash: string}) { if (args.email && args.passwordHash) { const user = await dataaccess.getUserByLogin(args.email, args.passwordHash); if (user && user.id) { @@ -80,7 +78,7 @@ class HomeRoute extends Route { return new GraphQLError("User is not logged in."); } }, - async register(args: any) { + async register(args: {username: string, email: string, passwordHash: string}) { if (args.username && args.email && args.passwordHash) { const user = await dataaccess.registerUser(args.username, args.email, args.passwordHash); if (user) { @@ -95,7 +93,7 @@ class HomeRoute extends Route { return new GraphQLError("No username, email or password given."); } }, - async vote(args: any) { + async vote(args: {postId: number, type: dataaccess.VoteType}) { if (args.postId && args.type) { if (req.session.userId) { return await (new Post(args.postId)).vote(req.session.userId, args.type); @@ -108,23 +106,34 @@ class HomeRoute extends Route { return new GraphQLError("No postId or type given."); } }, + async createPost(args: {content: string}) { + if (args.content) { + if (req.session.userId) { + return await dataaccess.createPost(args.content, req.session.userId); + } else { + res.status(status.UNAUTHORIZED); + return new GraphQLError("Not logged in."); + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("Can't create empty post."); + } + }, + async deletePost(args: {postId: number}) { + if (args.postId) { + const post = new Post(args.postId); + if ((await post.author()).id === req.session.userId) { + return await dataaccess.deletePost(post.id); + } else { + res.status(status.FORBIDDEN); + return new GraphQLError("User is not author of the post."); + } + } else { + return new GraphQLError("No postId given."); + } + }, }; } - - /** - * Configures the route. - */ - private configure() { - this.router.get("/", (req, res) => { - res.render("home"); - }); - this.router.get("/login", (req, res) => { - res.render("login"); - }); - this.router.get("/register", (req, res) => { - res.render("register"); - }); - } } export default HomeRoute; diff --git a/src/views/home/feed.pug b/src/views/home/feed.pug deleted file mode 100644 index e3758e1..0000000 --- a/src/views/home/feed.pug +++ /dev/null @@ -1,13 +0,0 @@ -div#feedcontainer - div.postinput - input(type=text placeholder='Post something') - button.submitbutton Submit - div.feeditem - div.itemhead - span.title Testuser - span.handle - a(href='#') @testuser - span.date 23.09.19 10:07 - p.text - | Example Test text. - | This is a test diff --git a/src/views/home/friends.pug b/src/views/home/friends.pug deleted file mode 100644 index ed36fb7..0000000 --- a/src/views/home/friends.pug +++ /dev/null @@ -1 +0,0 @@ -div#friendscontainer diff --git a/src/views/home/index.pug b/src/views/home/index.pug deleted file mode 100644 index 7da0198..0000000 --- a/src/views/home/index.pug +++ /dev/null @@ -1,9 +0,0 @@ -html - head - title Greenvironment Network - include ../includes/head - body - div#content - include ../includes/stylebar - include feed - include friends diff --git a/src/views/includes/head.pug b/src/views/includes/head.pug deleted file mode 100644 index 9e917e6..0000000 --- a/src/views/includes/head.pug +++ /dev/null @@ -1 +0,0 @@ -link(rel='stylesheet' href='stylesheets/style.css') diff --git a/src/views/includes/stylebar.pug b/src/views/includes/stylebar.pug deleted file mode 100644 index c6c36e3..0000000 --- a/src/views/includes/stylebar.pug +++ /dev/null @@ -1,2 +0,0 @@ -div.stylebar - h1 Greenvironment diff --git a/src/views/login/index.pug b/src/views/login/index.pug deleted file mode 100644 index 81c7585..0000000 --- a/src/views/login/index.pug +++ /dev/null @@ -1,8 +0,0 @@ -html - head - title Greenvironment Network Login - include ../includes/head - body - div#content - include ../includes/stylebar - include login diff --git a/src/views/login/login.pug b/src/views/login/login.pug deleted file mode 100644 index f205c95..0000000 --- a/src/views/login/login.pug +++ /dev/null @@ -1,6 +0,0 @@ -div#input-login - input(type=text placeholder='username') - input(type=text placeholder='password') - button.loginButton Login - a(href="/register" ) - | You aren´t part of greenvironment yet? - create a new account diff --git a/src/views/register/index.pug b/src/views/register/index.pug deleted file mode 100644 index 7ade6a7..0000000 --- a/src/views/register/index.pug +++ /dev/null @@ -1,8 +0,0 @@ -html - head - title Greenvironment Network Register - include ../includes/head - body - div#content - include ../includes/stylebar - include register diff --git a/src/views/register/register.pug b/src/views/register/register.pug deleted file mode 100644 index baaf1e6..0000000 --- a/src/views/register/register.pug +++ /dev/null @@ -1,8 +0,0 @@ -div#input-register - input(type=text placeholder='username') - input(type=text placeholder='email') - input(type=text placeholder='password') - input(type=text placeholder='repeat password') - button.registerButton Register - a(href="/login" ) - | You are already part of greenvironment? - login From d7f819e02e18ee73395efe9b1f44d0f46a180583 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Fri, 27 Sep 2019 20:48:16 +0200 Subject: [PATCH 07/14] More graphql implementation stuff - implemented methods to get user and post information - implemented methods to create chatrooms --- package-lock.json | 41 +++++-------------- src/app.ts | 2 +- src/lib/QueryHelper.ts | 2 +- src/lib/dataaccess/index.ts | 46 +++++++++++++++++++++ src/lib/regex.ts | 11 +++++ src/public/graphql/schema.graphql | 13 +++--- src/routes/home.ts | 68 +++++++++++++++++++++++-------- 7 files changed, 130 insertions(+), 53 deletions(-) create mode 100644 src/lib/regex.ts diff --git a/package-lock.json b/package-lock.json index 34608d6..4d87280 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2532,8 +2532,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -2554,14 +2553,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2576,20 +2573,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -2706,8 +2700,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -2719,7 +2712,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2734,7 +2726,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2742,14 +2733,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2768,7 +2757,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2849,8 +2837,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -2862,7 +2849,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -2948,8 +2934,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -2985,7 +2970,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3005,7 +2989,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3049,14 +3032,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, diff --git a/src/app.ts b/src/app.ts index 7ea9b9b..5801771 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,7 +10,7 @@ import {importSchema} from "graphql-import"; import * as http from "http"; import * as path from "path"; import * as socketIo from "socket.io"; -import dataaccess from "./lib/dataaccess"; +import dataaccess, {queryHelper} from "./lib/dataaccess"; import globals from "./lib/globals"; import routes from "./routes"; diff --git a/src/lib/QueryHelper.ts b/src/lib/QueryHelper.ts index 95b7a55..fb1b638 100644 --- a/src/lib/QueryHelper.ts +++ b/src/lib/QueryHelper.ts @@ -54,7 +54,7 @@ export class SqlTransaction { /** * Releases the client back to the pool. */ - public async release() { + public release() { this.client.release(); } } diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index c0816e6..db72696 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -1,6 +1,7 @@ import {Pool} from "pg"; import globals from "../globals"; import {QueryHelper} from "../QueryHelper"; +import {Chatroom} from "./Chatroom"; import {Post} from "./Post"; import {Profile} from "./Profile"; import {User} from "./User"; @@ -89,6 +90,22 @@ namespace dataaccess { return new Profile(result.id, result); } + /** + * Returns a post for a given postId.s + * @param postId + */ + export async function getPost(postId: number) { + 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; + } + } + /** * Creates a post * @param content @@ -115,6 +132,35 @@ namespace dataaccess { return true; } + /** + * Creates a chatroom containing two users + * @param members + */ + export async function createChat(...members: number[]) { + const idResult = await queryHelper.first({ + text: "INSERT INTO chats (id) values (nextval('chats_id_seq'::regclass)) 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 (ABSOLUTE chat, member) VALUES ($1, $2);", + values: [member], + }); + } + await transaction.commit(); + } catch (err) { + globals.logger.warn(`Failed to insert chatmember into database: ${err.message}`); + globals.logger.debug(err.stack); + } finally { + transaction.release(); + } + return new Chatroom(id); + } + /** * Enum representing the types of votes that can be performed on a post. */ diff --git a/src/lib/regex.ts b/src/lib/regex.ts new file mode 100644 index 0000000..1ed6c11 --- /dev/null +++ b/src/lib/regex.ts @@ -0,0 +1,11 @@ +export namespace is { + const emailRegex = /\S+?@\S+?(\.\S+?)?\.\w{2,3}(.\w{2-3})?/g + + /** + * Tests if a string is a valid email. + * @param testString + */ + export function email(testString: string) { + return emailRegex.test(testString) + } +} diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index bb369f5..60c32e3 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -1,18 +1,21 @@ type Query { - "returns the user object for a given user id" - getUser(userId: ID): User + "returns the user object for a given user id or a handle (only one required)" + getUser(userId: ID, handle: String): User "returns the logged in user" getSelf: User "returns the post object for a post id" - getPost(postId: ID): Post + getPost(postId: ID!): Post "returns the chat object for a chat id" - getChat(chatId: ID): ChatRoom + getChat(chatId: ID!): ChatRoom + + "Creates a chat between the user (and optional an other user)" + createChat(members: [ID!]): ChatRoom "returns the request object for a request id" - getRequest(requestId: ID): Request + getRequest(requestId: ID!): Request "find a post by the posted date or content" findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post] diff --git a/src/routes/home.ts b/src/routes/home.ts index 2a88931..1bb397b 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -5,6 +5,7 @@ import {Server} from "socket.io"; import dataaccess from "../lib/dataaccess"; import {Post} from "../lib/dataaccess/Post"; import {Profile} from "../lib/dataaccess/Profile"; +import {is} from "../lib/regex"; import Route from "../lib/Route"; /** @@ -50,13 +51,31 @@ class HomeRoute extends Route { return new GraphQLError("Not logged in"); } }, + async getUser({userId, handle}: {userId: number, handle: string}) { + if (handle) { + return await dataaccess.getUserByHandle(handle); + } else if (userId) { + return dataaccess.getUser(userId); + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No userId or handle provided."); + } + }, + async getPost({postId}: {postId: number}) { + if (postId) { + return await dataaccess.getPost(postId); + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No postId given."); + } + }, acceptCookies() { req.session.cookiesAccepted = true; return true; }, - async login(args: {email: string, passwordHash: string}) { - if (args.email && args.passwordHash) { - const user = await dataaccess.getUserByLogin(args.email, args.passwordHash); + async login({email, passwordHash}: {email: string, passwordHash: string}) { + if (email && passwordHash) { + const user = await dataaccess.getUserByLogin(email, passwordHash); if (user && user.id) { req.session.userId = user.id; return user; @@ -75,12 +94,16 @@ class HomeRoute extends Route { return true; } else { res.status(status.UNAUTHORIZED); - return new GraphQLError("User is not logged in."); + return new GraphQLError("Not logged in."); } }, - async register(args: {username: string, email: string, passwordHash: string}) { - if (args.username && args.email && args.passwordHash) { - const user = await dataaccess.registerUser(args.username, args.email, args.passwordHash); + async register({username, email, passwordHash}: {username: string, email: string, passwordHash: string}) { + if (username && email && passwordHash) { + if (!is.email(email)) { + res.status(status.BAD_REQUEST); + return new GraphQLError(`'${email}' is not a valid email address!`); + } + const user = await dataaccess.registerUser(username, email, passwordHash); if (user) { req.session.userId = user.id; return user; @@ -93,10 +116,10 @@ class HomeRoute extends Route { return new GraphQLError("No username, email or password given."); } }, - async vote(args: {postId: number, type: dataaccess.VoteType}) { - if (args.postId && args.type) { + async vote({postId, type}: {postId: number, type: dataaccess.VoteType}) { + if (postId && type) { if (req.session.userId) { - return await (new Post(args.postId)).vote(req.session.userId, args.type); + return await (new Post(postId)).vote(req.session.userId, type); } else { res.status(status.UNAUTHORIZED); return new GraphQLError("Not logged in."); @@ -106,10 +129,10 @@ class HomeRoute extends Route { return new GraphQLError("No postId or type given."); } }, - async createPost(args: {content: string}) { - if (args.content) { + async createPost({content}: {content: string}) { + if (content) { if (req.session.userId) { - return await dataaccess.createPost(args.content, req.session.userId); + return await dataaccess.createPost(content, req.session.userId); } else { res.status(status.UNAUTHORIZED); return new GraphQLError("Not logged in."); @@ -119,9 +142,9 @@ class HomeRoute extends Route { return new GraphQLError("Can't create empty post."); } }, - async deletePost(args: {postId: number}) { - if (args.postId) { - const post = new Post(args.postId); + async deletePost({postId}: {postId: number}) { + if (postId) { + const post = new Post(postId); if ((await post.author()).id === req.session.userId) { return await dataaccess.deletePost(post.id); } else { @@ -132,6 +155,19 @@ class HomeRoute extends Route { return new GraphQLError("No postId given."); } }, + async createChat({members}: {members: number[]}) { + if (req.session.userId) { + const chatMembers = [req.session.userId]; + if (members) { + chatMembers.push(...members); + } + return await dataaccess.createChat(...chatMembers); + + } else { + res.status(status.UNAUTHORIZED); + return new GraphQLError("Not logged in."); + } + }, }; } } From c97d0ffe55e399ec11ee6057e36e707857b2bdd4 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 28 Sep 2019 22:08:48 +0200 Subject: [PATCH 08/14] Api functions - added chat message functions - added markdown rendering - added custom error classes --- package-lock.json | 67 +++++++++++++++++++++++++---- package.json | 6 ++- src/default-config.yaml | 4 ++ src/lib/dataaccess/ChatMessage.ts | 8 ++++ src/lib/dataaccess/Chatroom.ts | 11 +++++ src/lib/dataaccess/DataObject.ts | 8 ++++ src/lib/dataaccess/Post.ts | 9 ++++ src/lib/dataaccess/Profile.ts | 22 ++++++++++ src/lib/dataaccess/User.ts | 1 + src/lib/dataaccess/index.ts | 51 +++++++++++++++------- src/lib/errors/BaseError.ts | 13 ++++++ src/lib/errors/ChatNotFoundError.ts | 7 +++ src/lib/errors/UserNotFoundError.ts | 7 +++ src/lib/errors/graphqlErrors.ts | 9 ++++ src/lib/markdown.ts | 36 ++++++++++++++++ src/public/graphql/schema.graphql | 33 +++++++++++--- src/routes/home.ts | 45 +++++++++++++++---- 17 files changed, 298 insertions(+), 39 deletions(-) create mode 100644 src/lib/errors/BaseError.ts create mode 100644 src/lib/errors/ChatNotFoundError.ts create mode 100644 src/lib/errors/UserNotFoundError.ts create mode 100644 src/lib/errors/graphqlErrors.ts create mode 100644 src/lib/markdown.ts diff --git a/package-lock.json b/package-lock.json index 4d87280..8e55c5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -171,6 +171,21 @@ "integrity": "sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==", "dev": true }, + "@types/linkify-it": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-2.1.0.tgz", + "integrity": "sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==", + "dev": true + }, + "@types/markdown-it": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-0.0.9.tgz", + "integrity": "sha512-IFSepyZXbF4dgSvsk8EsgaQ/8Msv1I5eTL0BZ0X3iGO9jw6tCVtPG8HchIPm3wrkmGdqZOD42kE0zplVi1gYDA==", + "dev": true, + "requires": { + "@types/linkify-it": "*" + } + }, "@types/mime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", @@ -178,9 +193,9 @@ "dev": true }, "@types/node": { - "version": "12.7.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.2.tgz", - "integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==", + "version": "12.7.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.8.tgz", + "integrity": "sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A==", "dev": true }, "@types/pg": { @@ -1894,6 +1909,11 @@ "has-binary2": "~1.0.2" } }, + "entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" + }, "env-variable": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", @@ -3329,12 +3349,6 @@ } } }, - "gulp-angular": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/gulp-angular/-/gulp-angular-0.1.2.tgz", - "integrity": "sha1-ljV2ul7qoDZqMf6l7S7AHvC0O3g=", - "dev": true - }, "gulp-minify": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/gulp-minify/-/gulp-minify-3.1.0.tgz", @@ -4214,6 +4228,14 @@ "resolve": "^1.1.7" } }, + "linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "requires": { + "uc.micro": "^1.0.1" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -4320,6 +4342,23 @@ "object-visit": "^1.0.0" } }, + "markdown-it": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", + "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", + "requires": { + "argparse": "^1.0.7", + "entities": "~2.0.0", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=" + }, "matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", @@ -4360,6 +4399,11 @@ "resolve-dir": "^1.0.0" } }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -6897,6 +6941,11 @@ "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", "dev": true }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", diff --git a/package.json b/package.json index 1d42af9..52e9130 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,13 @@ "@types/graphql": "^14.2.3", "@types/http-status": "^0.2.30", "@types/js-yaml": "^3.12.1", - "@types/node": "^12.7.2", + "@types/markdown-it": "0.0.9", + "@types/node": "^12.7.8", "@types/pg": "^7.11.0", "@types/socket.io": "^2.1.2", "@types/winston": "^2.4.4", "delete": "^1.1.0", "gulp": "^4.0.2", - "gulp-angular": "^0.1.2", "gulp-minify": "^3.1.0", "gulp-sass": "^4.0.2", "gulp-typescript": "^5.0.1", @@ -58,6 +58,8 @@ "graphql-import": "^0.7.1", "http-status": "^1.3.2", "js-yaml": "^3.13.1", + "markdown-it": "^10.0.0", + "markdown-it-emoji": "^1.4.0", "pg": "^7.12.1", "pug": "^2.0.4", "socket.io": "^2.2.0", diff --git a/src/default-config.yaml b/src/default-config.yaml index e232512..e607f15 100644 --- a/src/default-config.yaml +++ b/src/default-config.yaml @@ -13,3 +13,7 @@ server: session: secret: REPLACE WITH SAFE RANDOM GENERATED SECRET cookieMaxAge: 604800000‬ # 7 days + +markdown: + plugins: + - 'markdown-it-emoji' diff --git a/src/lib/dataaccess/ChatMessage.ts b/src/lib/dataaccess/ChatMessage.ts index 5930679..afbc600 100644 --- a/src/lib/dataaccess/ChatMessage.ts +++ b/src/lib/dataaccess/ChatMessage.ts @@ -1,7 +1,15 @@ +import markdown from "../markdown"; import {Chatroom} from "./Chatroom"; import {User} from "./User"; export class ChatMessage { constructor(public author: User, public chat: Chatroom, public timestamp: number, public content: string) { } + + /** + * The content rendered by markdown-it. + */ + public htmlContent(): string { + return markdown.renderInline(this.content); + } } diff --git a/src/lib/dataaccess/Chatroom.ts b/src/lib/dataaccess/Chatroom.ts index c6fe144..080a285 100644 --- a/src/lib/dataaccess/Chatroom.ts +++ b/src/lib/dataaccess/Chatroom.ts @@ -6,6 +6,17 @@ export class Chatroom { constructor(private id: number) {} + /** + * Returns if the chat exists. + */ + public async exists(): Promise { + 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. */ diff --git a/src/lib/dataaccess/DataObject.ts b/src/lib/dataaccess/DataObject.ts index 019bcc3..9c598ac 100644 --- a/src/lib/dataaccess/DataObject.ts +++ b/src/lib/dataaccess/DataObject.ts @@ -7,6 +7,14 @@ export abstract class DataObject { constructor(public id: number, protected row?: any) { } + /** + * Returns if the object extists by trying to load data. + */ + public async exists() { + await this.loadDataIfNotExists(); + return this.dataLoaded; + } + protected abstract loadData(): Promise; /** diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts index 08f9c4b..dffec19 100644 --- a/src/lib/dataaccess/Post.ts +++ b/src/lib/dataaccess/Post.ts @@ -1,3 +1,4 @@ +import markdown from "../markdown"; import {DataObject} from "./DataObject"; import {queryHelper} from "./index"; import dataaccess from "./index"; @@ -40,6 +41,14 @@ export class Post extends DataObject { return this.$content; } + /** + * the content rendered by markdown-it. + */ + public async htmlContent(): Promise { + await this.loadDataIfNotExists(); + return markdown.render(this.$content); + } + /** * The date the post was created at. */ diff --git a/src/lib/dataaccess/Profile.ts b/src/lib/dataaccess/Profile.ts index a3f3a46..33eb7eb 100644 --- a/src/lib/dataaccess/Profile.ts +++ b/src/lib/dataaccess/Profile.ts @@ -1,7 +1,29 @@ +import {Chatroom} from "./Chatroom"; import {queryHelper} from "./index"; import {User} from "./User"; export class Profile extends User { + + /** + * Returns all chatrooms (with pagination). + * @param first + * @param offset + */ + public async chats({first, offset}: {first: number, offset?: number}) { + 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 []; + } + } + /** * Sets the greenpoints of a user. * @param points diff --git a/src/lib/dataaccess/User.ts b/src/lib/dataaccess/User.ts index de5b25e..b29dfcc 100644 --- a/src/lib/dataaccess/User.ts +++ b/src/lib/dataaccess/User.ts @@ -8,6 +8,7 @@ export class User extends DataObject { private $email: string; private $greenpoints: number; private $joinedAt: string; + private $exists: boolean; /** * The name of the user diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index db72696..3db84ab 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -1,6 +1,9 @@ import {Pool} from "pg"; +import {ChatNotFoundError} from "../errors/ChatNotFoundError"; +import {UserNotFoundError} from "../errors/UserNotFoundError"; import globals from "../globals"; import {QueryHelper} from "../QueryHelper"; +import {ChatMessage} from "./ChatMessage"; import {Chatroom} from "./Chatroom"; import {Post} from "./Post"; import {Profile} from "./Profile"; @@ -27,6 +30,9 @@ 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; @@ -39,24 +45,20 @@ namespace dataaccess { await queryHelper.createTables(); } - /** - * Returns the user by id - * @param userId - */ - export function getUser(userId: number) { - return new User(userId); - } - /** * Returns the user by handle. * @param userHandle */ - export async function getUserByHandle(userHandle: string) { + export async function getUserByHandle(userHandle: string): Promise { const result = await queryHelper.first({ text: "SELECT * FROM users WHERE users.handle = $1", values: [userHandle], }); - return new User(result.id, result); + if (result) { + return new User(result.id, result); + } else { + throw new UserNotFoundError(userHandle); + } } /** @@ -72,7 +74,7 @@ namespace dataaccess { if (result) { return new Profile(result.id, result); } else { - return null; + throw new UserNotFoundError(email); } } @@ -94,7 +96,7 @@ namespace dataaccess { * Returns a post for a given postId.s * @param postId */ - export async function getPost(postId: number) { + export async function getPost(postId: number): Promise { const result = await queryHelper.first({ text: "SELECT * FROM posts WHERE id = $1", values: [postId], @@ -112,7 +114,7 @@ namespace dataaccess { * @param authorId * @param type */ - export async function createPost(content: string, authorId: number, type?: string) { + export async function createPost(content: string, authorId: number, type?: string): Promise { const result = await queryHelper.first({ text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *", values: [content, authorId, type], @@ -124,7 +126,7 @@ namespace dataaccess { * Deletes a post * @param postId */ - export async function deletePost(postId: number) { + export async function deletePost(postId: number): Promise { const result = await queryHelper.first({ text: "DELETE FROM posts WHERE posts.id = $1", values: [postId], @@ -136,7 +138,7 @@ namespace dataaccess { * Creates a chatroom containing two users * @param members */ - export async function createChat(...members: number[]) { + export async function createChat(...members: number[]): Promise { const idResult = await queryHelper.first({ text: "INSERT INTO chats (id) values (nextval('chats_id_seq'::regclass)) RETURNING *;", }); @@ -161,6 +163,25 @@ namespace dataaccess { return new Chatroom(id); } + /** + * 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, created_at) values ($1, $2, $3) RETURNING *", + values: [chatId, authorId, content], + }); + return new ChatMessage(new User(result.author), chat, result.timestamp, result.content); + } else { + throw new ChatNotFoundError(chatId); + } + } + /** * Enum representing the types of votes that can be performed on a post. */ diff --git a/src/lib/errors/BaseError.ts b/src/lib/errors/BaseError.ts new file mode 100644 index 0000000..f99171d --- /dev/null +++ b/src/lib/errors/BaseError.ts @@ -0,0 +1,13 @@ +import {GraphQLError} from "graphql"; + +/** + * Base error class. + */ +export class BaseError extends Error { + public readonly graphqlError: GraphQLError; + + constructor(message?: string, friendlyMessage?: string) { + super(message); + this.graphqlError = new GraphQLError(friendlyMessage || message); + } +} diff --git a/src/lib/errors/ChatNotFoundError.ts b/src/lib/errors/ChatNotFoundError.ts new file mode 100644 index 0000000..1d03525 --- /dev/null +++ b/src/lib/errors/ChatNotFoundError.ts @@ -0,0 +1,7 @@ +import {BaseError} from "./BaseError"; + +export class ChatNotFoundError extends BaseError { + constructor(chatId: number) { + super(`Chat with id ${chatId} not found.`); + } +} diff --git a/src/lib/errors/UserNotFoundError.ts b/src/lib/errors/UserNotFoundError.ts new file mode 100644 index 0000000..7869242 --- /dev/null +++ b/src/lib/errors/UserNotFoundError.ts @@ -0,0 +1,7 @@ +import {BaseError} from "./BaseError"; + +export class UserNotFoundError extends BaseError { + constructor(username: string) { + super(`User ${username} not found!`); + } +} diff --git a/src/lib/errors/graphqlErrors.ts b/src/lib/errors/graphqlErrors.ts new file mode 100644 index 0000000..9784712 --- /dev/null +++ b/src/lib/errors/graphqlErrors.ts @@ -0,0 +1,9 @@ +import {GraphQLError} from "graphql"; +import {BaseError} from "./BaseError"; + +export class NotLoggedInGqlError extends GraphQLError { + + constructor() { + super("Not logged in"); + } +} diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts new file mode 100644 index 0000000..fb164ec --- /dev/null +++ b/src/lib/markdown.ts @@ -0,0 +1,36 @@ +import * as MarkdownIt from "markdown-it/lib"; +import globals from "./globals"; + +namespace markdown { + + const md = new MarkdownIt(); + + for (const pluginName of globals.config.markdown.plugins) { + try { + const plugin = require(pluginName); + if (plugin) { + md.use(plugin); + } + } catch (err) { + globals.logger.warn(`Markdown-it plugin '${pluginName}' not found!`); + } + } + + /** + * Renders the markdown string inline (without blocks). + * @param markdownString + */ + export function renderInline(markdownString: string) { + return md.renderInline(markdownString); + } + + /** + * Renders the markdown string. + * @param markdownString + */ + export function render(markdownString: string) { + return md.render(markdownString); + } +} + +export default markdown; diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index 60c32e3..2ff19cf 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -53,7 +53,7 @@ type Mutation { denyRequest(requestId: ID!): Boolean "send a message in a Chatroom" - sendMessage(chatId: ID!, content: String!): Boolean + sendMessage(chatId: ID!, content: String!): ChatMessage "create the post" createPost(content: String!): Boolean @@ -70,6 +70,9 @@ type User { "name of the User" name: String! + "returns the chatrooms the user joined." + chats(first: Int=10, offset: Int): [ChatRoom] + "unique identifier name from the User" handle: String! @@ -85,7 +88,7 @@ type User { "creation date of the user account" joinedAt: String! - "returns all friends of the user" + "all friends of the user" friends: [User] "all request for groupChats/friends/events" @@ -95,9 +98,12 @@ type User { "represents a single user post" type Post { - "returns the text of the post" + "the text of the post" content: String + "the content of the post rendered by markdown-it" + htmlContent: String + "upvotes of the Post" upvotes: Int! @@ -110,7 +116,7 @@ type Post { "date the post was created" creationDate: String! - "returns the type of vote the user performed on the post" + "the type of vote the user performed on the post" userVote: VoteType } @@ -135,12 +141,29 @@ type ChatRoom { members: [User!] "return a specfic range of messages posted in the chat" - getMessages(first: Int, offset: Int): [String] + getMessages(first: Int, offset: Int): [ChatMessage] "id of the chat" id: ID! } +type ChatMessage { + "The author of the chat message." + author: User + + "The chatroom the message was posted in" + chat: ChatRoom + + "The timestamp when the message was posted (epoch)." + timestamp: Int + + "The content of the message." + content: String + + "The content of the message rendered by markdown-it." + htmlContent: String +} + "represents the type of vote performed on a post" enum VoteType { UPVOTE diff --git a/src/routes/home.ts b/src/routes/home.ts index 1bb397b..cd9fec9 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -3,8 +3,12 @@ import {GraphQLError} from "graphql"; import * as status from "http-status"; import {Server} from "socket.io"; import dataaccess from "../lib/dataaccess"; +import {Chatroom} from "../lib/dataaccess/Chatroom"; 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 {is} from "../lib/regex"; import Route from "../lib/Route"; @@ -48,14 +52,14 @@ class HomeRoute extends Route { return new Profile(req.session.userId); } else { res.status(status.UNAUTHORIZED); - return new GraphQLError("Not logged in"); + return new NotLoggedInGqlError(); } }, async getUser({userId, handle}: {userId: number, handle: string}) { if (handle) { return await dataaccess.getUserByHandle(handle); } else if (userId) { - return dataaccess.getUser(userId); + return new User(userId); } else { res.status(status.BAD_REQUEST); return new GraphQLError("No userId or handle provided."); @@ -69,19 +73,28 @@ class HomeRoute extends Route { return new GraphQLError("No postId given."); } }, + async getChat({chatId}: {chatId: number}) { + if (chatId) { + return new Chatroom(chatId); + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No chatId given."); + } + }, acceptCookies() { req.session.cookiesAccepted = true; return true; }, async login({email, passwordHash}: {email: string, passwordHash: string}) { if (email && passwordHash) { - const user = await dataaccess.getUserByLogin(email, passwordHash); - if (user && user.id) { + try { + const user = await dataaccess.getUserByLogin(email, passwordHash); req.session.userId = user.id; return user; - } else { + } catch (err) { + globals.logger.verbose(`Failed to login user '${email}'`); res.status(status.BAD_REQUEST); - return new GraphQLError("Invalid login data."); + return err.graphqlError; } } else { res.status(status.BAD_REQUEST); @@ -94,7 +107,7 @@ class HomeRoute extends Route { return true; } else { res.status(status.UNAUTHORIZED); - return new GraphQLError("Not logged in."); + return new NotLoggedInGqlError(); } }, async register({username, email, passwordHash}: {username: string, email: string, passwordHash: string}) { @@ -165,7 +178,23 @@ class HomeRoute extends Route { } else { res.status(status.UNAUTHORIZED); - return new GraphQLError("Not logged in."); + return new NotLoggedInGqlError(); + } + }, + async sendChatMessage({chatId, content}: {chatId: number, content: string}) { + if (!req.session.userId) { + return new NotLoggedInGqlError(); + } + if (chatId && content) { + try { + return await dataaccess.sendChatMessage(req.session.userId, chatId, content); + } catch (err) { + res.status(status.BAD_REQUEST); + return err.graphqlError; + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No chatId or content given."); } }, }; From e1a9287641295e83c9f0df241c0b8477b95b17a0 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 28 Sep 2019 23:30:49 +0200 Subject: [PATCH 09/14] Updated database for requests --- src/lib/dataaccess/User.ts | 19 ++++ src/lib/dataaccess/index.ts | 2 +- src/routes/home.ts | 4 +- src/sql/create-tables.sql | 190 ++++++++++++++++++++++++------------ src/sql/update-tables.sql | 22 +++-- 5 files changed, 162 insertions(+), 75 deletions(-) diff --git a/src/lib/dataaccess/User.ts b/src/lib/dataaccess/User.ts index b29dfcc..6358e59 100644 --- a/src/lib/dataaccess/User.ts +++ b/src/lib/dataaccess/User.ts @@ -61,6 +61,25 @@ export class User extends DataObject { return new Date(this.$joinedAt); } + /** + * Returns all friends of the user. + */ + public async friends(): Promise { + const result = await queryHelper.all({ + 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. */ diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 3db84ab..922fdac 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -41,8 +41,8 @@ namespace dataaccess { * Initializes everything that needs to be initialized asynchronous. */ export async function init() { - await queryHelper.updateTableDefinitions(); await queryHelper.createTables(); + await queryHelper.updateTableDefinitions(); } /** diff --git a/src/routes/home.ts b/src/routes/home.ts index cd9fec9..0ce10ad 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -135,7 +135,7 @@ class HomeRoute extends Route { return await (new Post(postId)).vote(req.session.userId, type); } else { res.status(status.UNAUTHORIZED); - return new GraphQLError("Not logged in."); + return new NotLoggedInGqlError(); } } else { res.status(status.BAD_REQUEST); @@ -148,7 +148,7 @@ class HomeRoute extends Route { return await dataaccess.createPost(content, req.session.userId); } else { res.status(status.UNAUTHORIZED); - return new GraphQLError("Not logged in."); + return new NotLoggedInGqlError(); } } else { res.status(status.BAD_REQUEST); diff --git a/src/sql/create-tables.sql b/src/sql/create-tables.sql index 17f6e74..a1563ac 100644 --- a/src/sql/create-tables.sql +++ b/src/sql/create-tables.sql @@ -1,65 +1,125 @@ -CREATE TABLE IF NOT EXISTS "user_sessions" ( - "sid" varchar NOT NULL COLLATE "default", - "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 varchar(16) 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 varchar(8) DEFAULT 'upvote' -); - -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) -); - -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 -); - -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 -); +--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; + + 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 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 tables +DO $$ BEGIN + + CREATE TABLE IF NOT EXISTS "user_sessions" ( + "sid" varchar NOT NULL COLLATE "default", + "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' + ); + + 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) + ); + + 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 + ); + + 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 + ); + + CREATE TABLE IF NOT EXISTS requests ( + sender SERIAL REFERENCES users (id) ON DELETE CASCADE, + receiver SERIAL REFERENCES users (id) ON DELETE CASCADE + ); + +END $$; diff --git a/src/sql/update-tables.sql b/src/sql/update-tables.sql index 13bc681..da794c7 100644 --- a/src/sql/update-tables.sql +++ b/src/sql/update-tables.sql @@ -1,8 +1,16 @@ -ALTER TABLE IF EXISTS votes - ADD COLUMN IF NOT EXISTS vote_type varchar(8) DEFAULT 'UPVOTE', - ALTER COLUMN vote_type SET DEFAULT 'UPVOTE'; +DO $$ BEGIN -ALTER TABLE IF EXISTS posts - ALTER COLUMN type SET DEFAULT 'MISC', - DROP COLUMN IF EXISTS upvotes, - DROP COLUMN IF EXISTS downvotes; + 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; + +END $$; From 47d775fd2c8f39e519a1de4808542eda76ee2073 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 29 Sep 2019 00:19:46 +0200 Subject: [PATCH 10/14] Bug Fixes - fixed Chatrooms - fixed ChatMessages --- src/lib/dataaccess/ChatMessage.ts | 2 +- src/lib/dataaccess/Chatroom.ts | 12 +++++++----- src/lib/dataaccess/DataObject.ts | 1 + src/lib/dataaccess/index.ts | 11 ++++++----- src/public/graphql/schema.graphql | 16 ++++++++-------- src/routes/home.ts | 2 +- src/sql/create-tables.sql | 2 +- 7 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/lib/dataaccess/ChatMessage.ts b/src/lib/dataaccess/ChatMessage.ts index afbc600..9ae0a12 100644 --- a/src/lib/dataaccess/ChatMessage.ts +++ b/src/lib/dataaccess/ChatMessage.ts @@ -3,7 +3,7 @@ import {Chatroom} from "./Chatroom"; import {User} from "./User"; export class ChatMessage { - constructor(public author: User, public chat: Chatroom, public timestamp: number, public content: string) { + constructor(public author: User, public chat: Chatroom, public createdAt: number, public content: string) { } /** diff --git a/src/lib/dataaccess/Chatroom.ts b/src/lib/dataaccess/Chatroom.ts index 080a285..6b839c6 100644 --- a/src/lib/dataaccess/Chatroom.ts +++ b/src/lib/dataaccess/Chatroom.ts @@ -4,7 +4,9 @@ import {User} from "./User"; export class Chatroom { - constructor(private id: number) {} + constructor(private readonly id: number) { + this.id = Number(id); + } /** * Returns if the chat exists. @@ -29,7 +31,7 @@ export class Chatroom { }); const chatMembers = []; for (const row of result) { - chatMembers.push(new User(row)); + chatMembers.push(new User(row.id, row)); } return chatMembers; } @@ -40,8 +42,8 @@ export class Chatroom { * @param offset - the offset of messages to return * @param containing - filter by containing */ - public async messages(limit?: number, offset?: number, containing?: string) { - const lim = limit || 16; + 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({ @@ -51,7 +53,7 @@ export class Chatroom { const messages = []; for (const row of result) { - messages.push(new ChatMessage(new User(row.author), this, row.timestamp, row.content)); + messages.push(new ChatMessage(new User(row.author), this, row.created_at, row.content)); } if (containing) { return messages.filter((x) => x.content.includes(containing)); diff --git a/src/lib/dataaccess/DataObject.ts b/src/lib/dataaccess/DataObject.ts index 9c598ac..2f89988 100644 --- a/src/lib/dataaccess/DataObject.ts +++ b/src/lib/dataaccess/DataObject.ts @@ -5,6 +5,7 @@ export abstract class DataObject { protected dataLoaded: boolean = false; constructor(public id: number, protected row?: any) { + this.id = Number(id); } /** diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 922fdac..11fba10 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -140,7 +140,7 @@ namespace dataaccess { */ export async function createChat(...members: number[]): Promise { const idResult = await queryHelper.first({ - text: "INSERT INTO chats (id) values (nextval('chats_id_seq'::regclass)) RETURNING *;", + text: "INSERT INTO chats (id) values (default) RETURNING *;", }); const id = idResult.id; const transaction = await queryHelper.createTransaction(); @@ -149,14 +149,15 @@ namespace dataaccess { for (const member of members) { await transaction.query({ name: "chat-member-insert", - text: "INSERT INTO chat_members (ABSOLUTE chat, member) VALUES ($1, $2);", - values: [member], + 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(); } @@ -173,10 +174,10 @@ namespace dataaccess { const chat = new Chatroom(chatId); if ((await chat.exists())) { const result = await queryHelper.first({ - text: "INSERT INTO chat_messages (chat, author, content, created_at) values ($1, $2, $3) RETURNING *", + text: "INSERT INTO chat_messages (chat, author, content) values ($1, $2, $3) RETURNING *", values: [chatId, authorId, content], }); - return new ChatMessage(new User(result.author), chat, result.timestamp, result.content); + return new ChatMessage(new User(result.author), chat, result.created_at, result.content); } else { throw new ChatNotFoundError(chatId); } diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index 2ff19cf..fb23b6f 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -11,9 +11,6 @@ type Query { "returns the chat object for a chat id" getChat(chatId: ID!): ChatRoom - "Creates a chat between the user (and optional an other user)" - createChat(members: [ID!]): ChatRoom - "returns the request object for a request id" getRequest(requestId: ID!): Request @@ -60,6 +57,9 @@ type Mutation { "delete the post for a given post id" deletePost(postId: ID!): Boolean + + "Creates a chat between the user (and optional an other user)" + createChat(members: [ID!]): ChatRoom } "represents a single user account" @@ -141,7 +141,7 @@ type ChatRoom { members: [User!] "return a specfic range of messages posted in the chat" - getMessages(first: Int, offset: Int): [ChatMessage] + messages(first: Int = 10, offset: Int, containing: String): [ChatMessage]! "id of the chat" id: ID! @@ -149,16 +149,16 @@ type ChatRoom { type ChatMessage { "The author of the chat message." - author: User + author: User! "The chatroom the message was posted in" - chat: ChatRoom + chat: ChatRoom! "The timestamp when the message was posted (epoch)." - timestamp: Int + createdAt: String! "The content of the message." - content: String + content: String! "The content of the message rendered by markdown-it." htmlContent: String diff --git a/src/routes/home.ts b/src/routes/home.ts index 0ce10ad..97e5943 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -181,7 +181,7 @@ class HomeRoute extends Route { return new NotLoggedInGqlError(); } }, - async sendChatMessage({chatId, content}: {chatId: number, content: string}) { + async sendMessage({chatId, content}: {chatId: number, content: string}) { if (!req.session.userId) { return new NotLoggedInGqlError(); } diff --git a/src/sql/create-tables.sql b/src/sql/create-tables.sql index a1563ac..486f50e 100644 --- a/src/sql/create-tables.sql +++ b/src/sql/create-tables.sql @@ -52,7 +52,7 @@ END$$; DO $$ BEGIN CREATE TABLE IF NOT EXISTS "user_sessions" ( - "sid" varchar NOT NULL COLLATE "default", + "sid" varchar NOT NULL, "sess" json NOT NULL, "expire" timestamp(6) NOT NULL, PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE From 7b94e4e3da213c09da80e0207ff48e3074aa9258 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 29 Sep 2019 13:03:38 +0200 Subject: [PATCH 11/14] Added in-memory caching - added class for caching management - added cache parameter to sql query object - implemented caching for sql querys with cache = true - added loglevel to config - improved some methods of dataaccess classes to enable better caching --- src/default-config.yaml | 3 ++ src/lib/MemoryCache.ts | 73 +++++++++++++++++++++++++++++++ src/lib/QueryHelper.ts | 27 ++++++++++-- src/lib/dataaccess/Chatroom.ts | 14 +++++- src/lib/dataaccess/Profile.ts | 6 ++- src/lib/dataaccess/User.ts | 5 +++ src/lib/globals.ts | 7 ++- src/public/graphql/schema.graphql | 60 +++++++++++++++++++++++-- 8 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 src/lib/MemoryCache.ts diff --git a/src/default-config.yaml b/src/default-config.yaml index e607f15..fda957a 100644 --- a/src/default-config.yaml +++ b/src/default-config.yaml @@ -17,3 +17,6 @@ session: markdown: plugins: - 'markdown-it-emoji' + +logging: + level: info diff --git a/src/lib/MemoryCache.ts b/src/lib/MemoryCache.ts new file mode 100644 index 0000000..e5b8a0b --- /dev/null +++ b/src/lib/MemoryCache.ts @@ -0,0 +1,73 @@ +import {EventEmitter} from "events"; +import * as crypto from "crypto"; + +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("md5"); + 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]; + } + } + } +} diff --git a/src/lib/QueryHelper.ts b/src/lib/QueryHelper.ts index fb1b638..01c98b8 100644 --- a/src/lib/QueryHelper.ts +++ b/src/lib/QueryHelper.ts @@ -11,6 +11,10 @@ import globals from "./globals"; const logger = globals.logger; +export interface IAdvancedQueryConfig extends QueryConfig { + cache?: boolean; +} + /** * Transaction class to wrap SQL transactions. */ @@ -101,7 +105,7 @@ export class QueryHelper { * executes the sql query with values and returns all results. * @param query */ - public async all(query: QueryConfig): Promise { + public async all(query: IAdvancedQueryConfig): Promise { const result = await this.query(query); return result.rows; } @@ -110,7 +114,7 @@ export class QueryHelper { * executes the sql query with values and returns the first result. * @param query */ - public async first(query: QueryConfig): Promise { + public async first(query: IAdvancedQueryConfig): Promise { const result = await this.query(query); if (result.rows && result.rows.length > 0) { return result.rows[0]; @@ -129,9 +133,24 @@ export class QueryHelper { * Queries the database with error handling. * @param query - the sql and values to execute */ - private async query(query: QueryConfig): Promise { + private async query(query: IAdvancedQueryConfig): Promise { try { - return await this.pool.query(query); + 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}`); diff --git a/src/lib/dataaccess/Chatroom.ts b/src/lib/dataaccess/Chatroom.ts index 6b839c6..0787637 100644 --- a/src/lib/dataaccess/Chatroom.ts +++ b/src/lib/dataaccess/Chatroom.ts @@ -1,3 +1,4 @@ +import globals from "../globals"; import {ChatMessage} from "./ChatMessage"; import {queryHelper} from "./index"; import {User} from "./User"; @@ -24,6 +25,7 @@ export class Chatroom { */ public async members(): Promise { 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;`, @@ -31,7 +33,8 @@ export class Chatroom { }); const chatMembers = []; for (const row of result) { - chatMembers.push(new User(row.id, row)); + const user = new User(row.id, row); + chatMembers.push(user); } return chatMembers; } @@ -47,13 +50,20 @@ export class Chatroom { 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) { - messages.push(new ChatMessage(new User(row.author), this, row.created_at, row.content)); + 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)); diff --git a/src/lib/dataaccess/Profile.ts b/src/lib/dataaccess/Profile.ts index 33eb7eb..957d631 100644 --- a/src/lib/dataaccess/Profile.ts +++ b/src/lib/dataaccess/Profile.ts @@ -6,10 +6,14 @@ 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}) { + public async chats({first, offset}: {first: number, offset?: number}): Promise { + if (!(await this.exists())) { + return []; + } first = first || 10; offset = offset || 0; diff --git a/src/lib/dataaccess/User.ts b/src/lib/dataaccess/User.ts index 6358e59..b3501cc 100644 --- a/src/lib/dataaccess/User.ts +++ b/src/lib/dataaccess/User.ts @@ -1,3 +1,4 @@ +import globals from "../globals"; import {DataObject} from "./DataObject"; import {queryHelper} from "./index"; import {Post} from "./Post"; @@ -47,6 +48,7 @@ export class User extends DataObject { */ public async numberOfPosts(): Promise { const result = await queryHelper.first({ + cache: true, text: "SELECT COUNT(*) count FROM posts WHERE author = $1", values: [this.id], }); @@ -66,6 +68,7 @@ export class User extends DataObject { */ public async friends(): Promise { const result = await queryHelper.all({ + cache: true, text: "SELECT * FROM user_friends WHERE user_id = $1 OR friend_id = $1", values: [this.id], }); @@ -87,6 +90,7 @@ export class User extends DataObject { 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], }); @@ -107,6 +111,7 @@ export class User extends DataObject { result = this.row; } else { result = await queryHelper.first({ + cache: true, text: "SELECT * FROM users WHERE users.id = $1", values: [this.id], }); diff --git a/src/lib/globals.ts b/src/lib/globals.ts index 3457fa5..ea0b8d8 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -8,6 +8,7 @@ import * as fsx from "fs-extra"; import * as yaml from "js-yaml"; import * as winston from "winston"; +import {MemoryCache} from "./Cache"; const configPath = "config.yaml"; const defaultConfig = __dirname + "/../default-config.yaml"; @@ -26,6 +27,7 @@ if (!(fsx.pathExistsSync(configPath))) { */ namespace globals { export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8")); + export const cache = new MemoryCache(1200); export const logger = winston.createLogger({ transports: [ new winston.transports.Console({ @@ -36,10 +38,13 @@ namespace globals { return `${timestamp} ${level}: ${message}`; }), ), - level: "debug", + level: config.logging.level, }), ], }); + 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; diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index fb23b6f..e82d4ec 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -3,7 +3,7 @@ type Query { getUser(userId: ID, handle: String): User "returns the logged in user" - getSelf: User + getSelf: Profile "returns the post object for a post id" getPost(postId: ID!): Post @@ -26,10 +26,10 @@ type Mutation { acceptCookies: Boolean "Login of the user. The passwordHash should be a sha512 hash of the password." - login(email: String, passwordHash: String): User + login(email: String, passwordHash: String): Profile "Registers the user." - register(username: String, email: String, passwordHash: String): User + register(username: String, email: String, passwordHash: String): Profile "Logout of the user." logout: Boolean @@ -62,8 +62,60 @@ type Mutation { createChat(members: [ID!]): ChatRoom } +interface UserData { + "url for the Profile picture of the User" + profilePicture: String + + "name of the User" + name: String! + + "unique identifier name from the User" + handle: String! + + "Id of the User" + id: ID! + + "the total number of posts the user posted" + numberOfPosts: Int + + "returns a given number of posts of a user" + posts(first: Int=10, offset: Int): [Post] + + "creation date of the user account" + joinedAt: String! + + "all friends of the user" + friends: [User] +} + "represents a single user account" -type User { +type User implements UserData{ + "url for the Profile picture of the User" + profilePicture: String + + "name of the User" + name: String! + + "unique identifier name from the User" + handle: String! + + "Id of the User" + id: ID! + + "the total number of posts the user posted" + numberOfPosts: Int + + "returns a given number of posts of a user" + posts(first: Int=10, offset: Int): [Post] + + "creation date of the user account" + joinedAt: String! + + "all friends of the user" + friends: [User] +} + +type Profile implements UserData { "url for the Profile picture of the User" profilePicture: String From 46733d104611ec7a4d0518558acb2dfa5e023fa9 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 29 Sep 2019 13:03:52 +0200 Subject: [PATCH 12/14] Fixed import filename for caching on globals --- src/lib/globals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/globals.ts b/src/lib/globals.ts index ea0b8d8..d3df954 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -8,7 +8,7 @@ import * as fsx from "fs-extra"; import * as yaml from "js-yaml"; import * as winston from "winston"; -import {MemoryCache} from "./Cache"; +import {MemoryCache} from "./MemoryCache"; const configPath = "config.yaml"; const defaultConfig = __dirname + "/../default-config.yaml"; From 770c8a35faf3f72d7714c006a26ba343f2466400 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 29 Sep 2019 13:28:13 +0200 Subject: [PATCH 13/14] Fixed data loading - fixed multiple loading requests on pending data load --- src/lib/MemoryCache.ts | 2 +- src/lib/dataaccess/DataObject.ts | 15 +++++++++++++-- src/lib/dataaccess/Post.ts | 4 ++++ src/lib/dataaccess/index.ts | 1 + src/public/graphql/schema.graphql | 7 +++++-- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/lib/MemoryCache.ts b/src/lib/MemoryCache.ts index e5b8a0b..17890d8 100644 --- a/src/lib/MemoryCache.ts +++ b/src/lib/MemoryCache.ts @@ -1,5 +1,5 @@ -import {EventEmitter} from "events"; import * as crypto from "crypto"; +import {EventEmitter} from "events"; export class MemoryCache extends EventEmitter { private cacheItems: any = {}; diff --git a/src/lib/dataaccess/DataObject.ts b/src/lib/dataaccess/DataObject.ts index 2f89988..26fc809 100644 --- a/src/lib/dataaccess/DataObject.ts +++ b/src/lib/dataaccess/DataObject.ts @@ -1,10 +1,14 @@ /** * abstact DataObject class */ -export abstract class DataObject { +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); } @@ -22,8 +26,15 @@ export abstract class DataObject { * Loads data from the database if data has not been loaded */ protected async loadDataIfNotExists() { - if (!this.dataLoaded) { + 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()); + }); } } } diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts index dffec19..2d3d8d6 100644 --- a/src/lib/dataaccess/Post.ts +++ b/src/lib/dataaccess/Post.ts @@ -16,6 +16,7 @@ export class Post extends DataObject { */ public async upvotes(): Promise { const result = await queryHelper.first({ + cache: true, text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'UPVOTE'", values: [this.id], }); @@ -27,6 +28,7 @@ export class Post extends DataObject { */ public async downvotes(): Promise { const result = await queryHelper.first({ + cache: true, text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'DOWNVOTE'", values: [this.id], }); @@ -80,6 +82,7 @@ export class Post extends DataObject { */ public async userVote(userId: number): Promise { 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], }); @@ -127,6 +130,7 @@ export class Post extends DataObject { result = this.row; } else { result = await queryHelper.first({ + cache: true, text: "SELECT * FROM posts WHERE posts.id = $1", values: [this.id], }); diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 11fba10..039d3d4 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -115,6 +115,7 @@ namespace dataaccess { * @param type */ export async function createPost(content: string, authorId: number, type?: string): Promise { + type = type || "MISC"; const result = await queryHelper.first({ text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *", values: [content, authorId, type], diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index e82d4ec..1629f16 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -53,7 +53,7 @@ type Mutation { sendMessage(chatId: ID!, content: String!): ChatMessage "create the post" - createPost(content: String!): Boolean + createPost(content: String!): Post "delete the post for a given post id" deletePost(postId: ID!): Boolean @@ -150,6 +150,9 @@ type Profile implements UserData { "represents a single user post" type Post { + "The id of the post." + id: ID! + "the text of the post" content: String @@ -166,7 +169,7 @@ type Post { author: User! "date the post was created" - creationDate: String! + createdAt: String! "the type of vote the user performed on the post" userVote: VoteType From b9655acc124485d65333b3891e1674964911beea Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 29 Sep 2019 17:09:07 +0200 Subject: [PATCH 14/14] Implemented Request API - implemented sendRequest, acceptRequest, denyRequest --- src/lib/dataaccess/ChatMessage.ts | 7 +- src/lib/dataaccess/Post.ts | 2 +- src/lib/dataaccess/Profile.ts | 94 ++++++++++++++++++++++++-- src/lib/dataaccess/Request.ts | 12 ++++ src/lib/dataaccess/index.ts | 17 +++++ src/lib/errors/RequestNotFoundError.ts | 9 +++ src/public/graphql/schema.graphql | 24 +++---- src/routes/home.ts | 46 +++++++++++++ src/sql/create-tables.sql | 16 +++-- src/sql/update-tables.sql | 3 + 10 files changed, 206 insertions(+), 24 deletions(-) create mode 100644 src/lib/dataaccess/Request.ts create mode 100644 src/lib/errors/RequestNotFoundError.ts diff --git a/src/lib/dataaccess/ChatMessage.ts b/src/lib/dataaccess/ChatMessage.ts index 9ae0a12..bbad8ad 100644 --- a/src/lib/dataaccess/ChatMessage.ts +++ b/src/lib/dataaccess/ChatMessage.ts @@ -3,8 +3,11 @@ import {Chatroom} from "./Chatroom"; import {User} from "./User"; export class ChatMessage { - constructor(public author: User, public chat: Chatroom, public createdAt: number, public content: string) { - } + constructor( + public readonly author: User, + public readonly chat: Chatroom, + public readonly createdAt: number, + public readonly content: string) {} /** * The content rendered by markdown-it. diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts index 2d3d8d6..7908f62 100644 --- a/src/lib/dataaccess/Post.ts +++ b/src/lib/dataaccess/Post.ts @@ -108,7 +108,7 @@ export class Post extends DataObject { } else { if (uVote) { await queryHelper.first({ - text: "UPDATE votes SET vote_type = $1 WHERE user_id = $1 AND item_id = $3", + text: "UPDATE votes SET vote_type = $1 WHERE user_id = $2 AND item_id = $3", values: [type, userId, this.id], }); } else { diff --git a/src/lib/dataaccess/Profile.ts b/src/lib/dataaccess/Profile.ts index 957d631..090e6da 100644 --- a/src/lib/dataaccess/Profile.ts +++ b/src/lib/dataaccess/Profile.ts @@ -1,6 +1,8 @@ +import {RequestNotFoundError} from "../errors/RequestNotFoundError"; import {Chatroom} from "./Chatroom"; -import {queryHelper} from "./index"; +import dataaccess, {queryHelper} from "./index"; import {User} from "./User"; +import {Request} from "./Request"; export class Profile extends User { @@ -28,6 +30,30 @@ export class Profile extends User { } } + /** + * 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 @@ -46,7 +72,7 @@ export class Profile extends User { */ public async setEmail(email: string): Promise { const result = await queryHelper.first({ - text: "UPDATE TABLE users SET email = $1 WHERE users.id = $2 RETURNING email", + text: "UPDATE users SET email = $1 WHERE users.id = $2 RETURNING email", values: [email, this.id], }); return result.email; @@ -57,7 +83,7 @@ export class Profile extends User { */ public async setHandle(handle: string): Promise { const result = await queryHelper.first({ - text: "UPDATE TABLE users SET handle = $1 WHERE id = $2", + text: "UPDATE users SET handle = $1 WHERE id = $2", values: [handle, this.id], }); return result.handle; @@ -69,9 +95,69 @@ export class Profile extends User { */ public async setName(name: string): Promise { const result = await queryHelper.first({ - text: "UPDATE TABLE users SET name = $1 WHERE id = $2", + 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; + } } diff --git a/src/lib/dataaccess/Request.ts b/src/lib/dataaccess/Request.ts new file mode 100644 index 0000000..f161757 --- /dev/null +++ b/src/lib/dataaccess/Request.ts @@ -0,0 +1,12 @@ +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) {} +} diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 039d3d4..28ae27c 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -7,6 +7,7 @@ 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; @@ -184,6 +185,22 @@ namespace dataaccess { } } + /** + * 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], + }); + return new Request(new User(result.sender), new User(result.receiver), result.type); + } + /** * Enum representing the types of votes that can be performed on a post. */ diff --git a/src/lib/errors/RequestNotFoundError.ts b/src/lib/errors/RequestNotFoundError.ts new file mode 100644 index 0000000..8a020d1 --- /dev/null +++ b/src/lib/errors/RequestNotFoundError.ts @@ -0,0 +1,9 @@ +import dataaccess from "../dataaccess"; +import {BaseError} from "./BaseError"; + +export class RequestNotFoundError extends BaseError { + constructor(sender: number, receiver: number, type: dataaccess.RequestType) { + super(`Request with sender '${sender}' and receiver '${receiver}' of type '${type}' not found.`); + } + +} diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index 1629f16..19a19eb 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -11,9 +11,6 @@ type Query { "returns the chat object for a chat id" getChat(chatId: ID!): ChatRoom - "returns the request object for a request id" - getRequest(requestId: ID!): Request - "find a post by the posted date or content" findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post] @@ -41,10 +38,10 @@ type Mutation { report(postId: ID!): Boolean "send a request" - sendRequest(reciever: ID!, type: RequestType): Boolean + sendRequest(receiver: ID!, type: RequestType): Request "lets you accept a request for a given request id" - acceptRequest(requestId: ID!): Boolean + acceptRequest(sender: ID!, type: RequestType): Boolean "lets you deny a request for a given request id" denyRequest(requestId: ID!): Boolean @@ -143,8 +140,11 @@ type Profile implements UserData { "all friends of the user" friends: [User] - "all request for groupChats/friends/events" - requests: [Request] + "all sent request for groupChats/friends/events" + sentRequests: [Request] + + "all received request for groupChats/friends/events" + receivedRequests: [Request] } "represents a single user post" @@ -177,9 +177,6 @@ type Post { "represents a request of any type" type Request { - "id of the request" - id: ID! - "Id of the user who sended the request" sender: User! @@ -187,7 +184,7 @@ type Request { receiver: User! "type of the request" - requestType: RequestType! + type: RequestType! } "represents a chatroom" @@ -225,7 +222,10 @@ enum VoteType { DOWNVOTE } -"represents the type of request that the user has received" +""" +represents the type of request that the user has received +Currently on Friend Requests are implemented. +""" enum RequestType { FRIENDREQUEST GROUPINVITE diff --git a/src/routes/home.ts b/src/routes/home.ts index 97e5943..88ce8b0 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -183,6 +183,7 @@ class HomeRoute extends Route { }, async sendMessage({chatId, content}: {chatId: number, content: string}) { if (!req.session.userId) { + res.status(status.UNAUTHORIZED); return new NotLoggedInGqlError(); } if (chatId && content) { @@ -197,6 +198,51 @@ class HomeRoute extends Route { return new GraphQLError("No chatId or content given."); } }, + async sendRequest({receiver, type}: {receiver: number, type: dataaccess.RequestType}) { + if (!req.session.userId) { + res.status(status.UNAUTHORIZED); + return new NotLoggedInGqlError(); + } + if (receiver && type) { + return await dataaccess.createRequest(req.session.userId, receiver, type); + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No receiver or type given."); + } + }, + async denyRequest({sender, type}: {sender: number, type: dataaccess.RequestType}) { + if (!req.session.userId) { + res.status(status.UNAUTHORIZED); + return new NotLoggedInGqlError(); + } + if (sender && type) { + const profile = new Profile(req.session.userId); + await profile.denyRequest(sender, type); + return true; + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No sender or type given."); + } + }, + async acceptRequest({sender, type}: {sender: number, type: dataaccess.RequestType}) { + if (!req.session.userId) { + res.status(status.UNAUTHORIZED); + return new NotLoggedInGqlError(); + } + if (sender && type) { + try { + const profile = new Profile(req.session.userId); + await profile.acceptRequest(sender, type); + return true; + } catch (err) { + res.status(status.BAD_REQUEST); + return err.graphqlError; + } + } else { + res.status(status.BAD_REQUEST); + return new GraphQLError("No sender or type given."); + } + }, }; } } diff --git a/src/sql/create-tables.sql b/src/sql/create-tables.sql index 486f50e..128492b 100644 --- a/src/sql/create-tables.sql +++ b/src/sql/create-tables.sql @@ -81,7 +81,8 @@ DO $$ BEGIN 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' + vote_type votetype DEFAULT 'DOWNVOTE', + PRIMARY KEY (user_id, item_id) ); CREATE TABLE IF NOT EXISTS events ( @@ -92,7 +93,8 @@ DO $$ BEGIN CREATE TABLE IF NOT EXISTS event_members ( event BIGSERIAL REFERENCES events (id), - member SERIAL REFERENCES users (id) + member SERIAL REFERENCES users (id), + PRIMARY KEY (event, member) ); CREATE TABLE IF NOT EXISTS chats ( @@ -109,17 +111,21 @@ DO $$ BEGIN CREATE TABLE IF NOT EXISTS chat_members ( chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE, - member SERIAL REFERENCES users (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 + 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 + receiver SERIAL REFERENCES users (id) ON DELETE CASCADE, + type requesttype DEFAULT 'FRIENDREQUEST', + PRIMARY KEY (sender, receiver, type) ); END $$; diff --git a/src/sql/update-tables.sql b/src/sql/update-tables.sql index da794c7..858a214 100644 --- a/src/sql/update-tables.sql +++ b/src/sql/update-tables.sql @@ -13,4 +13,7 @@ DO $$ BEGIN DROP COLUMN IF EXISTS upvotes, DROP COLUMN IF EXISTS downvotes; + ALTER TABLE requests + ADD COLUMN IF NOT EXISTS type requesttype DEFAULT 'FRIENDREQUEST'; + END $$;