diff --git a/CHANGELOG.md b/CHANGELOG.md index ddc745c..c7d9c5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,3 +12,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Graphql Schema - default-config file and generation of config file on startup - DTOs +- Home Route diff --git a/package-lock.json b/package-lock.json index bb5ff31..7baeb51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1039,9 +1039,9 @@ } }, "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, "requires": { "anymatch": "^2.0.0", @@ -1858,9 +1858,9 @@ } }, "es5-ext": { - "version": "0.10.50", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", - "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==", + "version": "0.10.51", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.51.tgz", + "integrity": "sha512-oRpWzM2WcLHVKpnrcyB7OW8j/s67Ba04JCm0WnNv3RiABSvs7mrQlutB8DBv793gKcp0XENR8Il8WxGTlZ73gQ==", "dev": true, "requires": { "es6-iterator": "~2.0.3", @@ -1880,13 +1880,13 @@ } }, "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.2.tgz", + "integrity": "sha512-/ZypxQsArlv+KHpGvng52/Iz8by3EQPxhmbuz8yFG89N/caTFBSbcXONDw0aMjy827gQg26XAjP4uXFvnfINmQ==", "dev": true, "requires": { - "d": "1", - "es5-ext": "~0.10.14" + "d": "^1.0.1", + "es5-ext": "^0.10.51" } }, "es6-weak-map": { @@ -2691,14 +2691,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" @@ -2717,7 +2715,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2897,8 +2894,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -3004,8 +3000,7 @@ "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -3026,6 +3021,11 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "g": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/g/-/g-2.0.1.tgz", + "integrity": "sha1-C1lj69DKcOO8jGdmk0oCGCHIuFc=" + }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -7013,9 +7013,9 @@ } }, "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true }, "uri-js": { diff --git a/package.json b/package.json index 8043e04..0fb8448 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "express-session": "^1.16.2", "express-socket.io-session": "^1.3.5", "fs-extra": "^8.1.0", + "g": "^2.0.1", "graphql": "^14.4.2", "graphql-import": "^0.7.1", "js-yaml": "^3.13.1", diff --git a/src/app.ts b/src/app.ts index c5086c1..c7079ac 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,7 +2,7 @@ import * as express from "express"; import * as http from "http"; import * as path from "path"; import * as socketIo from "socket.io"; -import {DTO} from "./lib/DTO"; +import dataaccess from "./lib/dataaccess"; import globals from "./lib/globals"; import routes from "./routes"; @@ -10,22 +10,22 @@ class App { public app: express.Application; public io: socketIo.Server; public server: http.Server; - public dto: DTO; constructor() { this.app = express(); this.server = new http.Server(this.app); this.io = socketIo(this.server); - this.dto = new DTO(); } /** * initializes everything that needs to be initialized asynchronous. */ public async init() { - await this.dto.init(); + await dataaccess.init(); + await routes.ioListeners(this.io); this.app.set("views", path.join(__dirname, "views")); this.app.set("view engine", "pug"); + this.app.use(express.static(path.join(__dirname, "public"))); this.app.use(routes.router); } diff --git a/src/index.ts b/src/index.ts index 4e814b5..e6c66b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,9 @@ -import * as fsx from "fs-extra"; import App from "./app"; -const configPath = "config.yaml"; -const defaultConfig = __dirname + "/default-config.yaml"; - /** * async main function wrapper. */ (async () => { - if (!(await fsx.pathExists(configPath))) { - await fsx.copy(defaultConfig, configPath); - } const app = new App(); await app.init(); app.start(); diff --git a/src/lib/DTO.ts b/src/lib/DTO.ts deleted file mode 100644 index 2d93484..0000000 --- a/src/lib/DTO.ts +++ /dev/null @@ -1,248 +0,0 @@ -import {Runtime} from "inspector"; -import {Pool} from "pg"; -import globals from "./globals"; -import {QueryHelper} from "./QueryHelper"; - -const config = globals.config; -const tableCreationFile = __dirname + "/../sql/create-tables.sql"; -const dbClient: Pool = new Pool({ - database: config.database.database, - host: config.database.host, - password: config.database.password, - port: config.database.port, - user: config.database.user, -}); -const queryHelper = new QueryHelper(dbClient, tableCreationFile); - -export class DTO { - private queryHelper: QueryHelper; - - constructor() { - this.queryHelper = queryHelper; - } - - /** - * Initializes everything that needs to be initialized asynchronous. - */ - public async init() { - await this.queryHelper.createTables(); - } - - /** - * Returns the user by id - * @param userId - */ - public getUser(userId: number) { - return new User(userId); - } - - /** - * Returns the user by handle. - * @param userHandle - */ - public async getUserByHandle(userHandle: string) { - const result = await this.queryHelper.first({ - text: "SELECT * FROM users WHERE users.handle = $1", - values: [userHandle], - }); - return new User(result.id, result); - } -} - -export class User { - public readonly id: number; - private $name: string; - private $handle: string; - private $email: string; - private $greenpoints: number; - private $joinedAt: string; - private dataLoaded: boolean; - - /** - * Constructor of the user - * @param id - * @param row - */ - constructor(id: number, private row?: any) { - this.id = id; - } - - /** - * The name of the user - */ - public async name(): Promise { - if (!this.dataLoaded) { - await this.loadData(); - } - 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. - */ - public async handle(): Promise { - if (!this.dataLoaded) { - await this.loadData(); - } - 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 - */ - public async email(): Promise { - if (!this.dataLoaded) { - await this.loadData(); - } - 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 - */ - public async greenpoints(): Promise { - if (!this.dataLoaded) { - await this.loadData(); - } - 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 - */ - public async joinedAt(): Promise { - if (!this.dataLoaded) { - await this.loadData(); - } - return new Date(this.$joinedAt); - } - - /** - * Fetches the data for the user. - */ - private async loadData(): Promise { - let result: any; - if (this.row) { - result = this.row; - } else { - result = await queryHelper.first({ - text: "SELECT * FROM users WHERE user.id = $1", - values: [this.id], - }); - } - if (result) { - this.$name = result.name; - this.$handle = result.handle; - this.$email = result.email; - this.$greenpoints = result.greenpoints; - this.$joinedAt = result.joined_at; - this.dataLoaded = true; - } - } -} - -export class Post { - public readonly id: number; - private $upvotes: number; - private $downvotes: number; - private $createdAt: string; - private $content: string; - private $author: number; - private $type: string; - private dataLoaded: boolean = false; - - constructor(id: number, private row?: any) { - this.id = id; - } - - /** - * Returns the upvotes of a post. - */ - public async upvotes() { - if (!this.dataLoaded) { - await this.loadData(); - } - return this.$upvotes; - } - - /** - * Returns the downvotes of the post - */ - public async downvotes() { - if (!this.dataLoaded) { - await this.loadData(); - } - return this.$downvotes; - } - - /** - * Loads the data from the database if needed. - */ - private async loadData(): Promise { - let result: any; - if (this.row) { - result = this.row; - } else { - result = await queryHelper.first({ - text: "SELECT * FROM posts WHERE posts.id = $1", - values: [this.id], - }); - } - 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/QueryHelper.ts b/src/lib/QueryHelper.ts index 3431994..5970f70 100644 --- a/src/lib/QueryHelper.ts +++ b/src/lib/QueryHelper.ts @@ -1,10 +1,24 @@ +/** + * @author Trivernis + * @remarks + * + * Taken from {@link https://github.com/Trivernis/whooshy} + */ + import * as fsx from "fs-extra"; import {Pool, PoolClient, QueryConfig, QueryResult} from "pg"; import globals from "./globals"; const logger = globals.logger; +/** + * Transaction class to wrap SQL transactions. + */ export class SqlTransaction { + /** + * Constructor. + * @param client + */ constructor(private client: PoolClient) { } @@ -45,10 +59,19 @@ export class SqlTransaction { } } +/** + * Query helper for easyer fetching of a specific row count. + */ export class QueryHelper { private pool: Pool; - constructor(pgPool: Pool, private tableCreationFile?: string) { + /** + * Constructor. + * @param pgPool + * @param [tableCreationFile] + * @param [tableUpdateFile] + */ + constructor(pgPool: Pool, private tableCreationFile?: string, private tableUpdateFile?: string) { this.pool = pgPool; } @@ -63,6 +86,17 @@ export class QueryHelper { } } + /** + * Updates the definition of the tables if the table update file was passed in the constructor + */ + public async updateTableDefinitions() { + if (this.tableUpdateFile) { + logger.info("Updating table definitions..."); + const tableSql = await fsx.readFile(this.tableUpdateFile, "utf-8"); + await this.query({text: tableSql}); + } + } + /** * executes the sql query with values and returns all results. * @param query diff --git a/src/lib/Route.ts b/src/lib/Route.ts new file mode 100644 index 0000000..63c25ac --- /dev/null +++ b/src/lib/Route.ts @@ -0,0 +1,27 @@ +/** + * @author Trivernis + * @remarks + * + * Taken from {@link https://github.com/Trivernis/whooshy} + */ + +import {Router} from "express"; +import {Namespace, Server} from "socket.io"; + +/** + * Abstract Route class to be implemented by each route. + * This class contains the socket-io Server, router and resolver + * for each route. + */ +abstract class Route { + + public router?: Router; + protected io?: Server; + protected ions?: Namespace; + + public abstract async init(...params: any): Promise; + public abstract async destroy(...params: any): Promise; + public abstract async resolver(request: any, response: any): Promise; +} + +export default Route; diff --git a/src/lib/dataaccess/DataObject.ts b/src/lib/dataaccess/DataObject.ts new file mode 100644 index 0000000..1890425 --- /dev/null +++ b/src/lib/dataaccess/DataObject.ts @@ -0,0 +1,20 @@ +/** + * abstact DataObject class + */ +export abstract class DataObject { + protected dataLoaded: boolean = false; + + constructor(public id: number, protected row?: any) { + } + + protected abstract loadData(): Promise; + + /** + * Loads data from the database if data has not been loaded + */ + protected loadDataIfNotExists() { + if (this.dataLoaded) { + this.loadData(); + } + } +} diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts new file mode 100644 index 0000000..b4193c4 --- /dev/null +++ b/src/lib/dataaccess/Post.ts @@ -0,0 +1,103 @@ +import {DataObject} from "./DataObject"; +import {queryHelper} from "./index"; +import dataaccess from "./index"; +import {User} from "./User"; + +export class Post extends DataObject { + public readonly id: number; + private $upvotes: number; + private $downvotes: number; + private $createdAt: string; + private $content: string; + private $author: number; + private $type: string; + + /** + * Returns the upvotes of a post. + */ + public async upvotes(): Promise { + this.loadDataIfNotExists(); + return this.$upvotes; + } + + /** + * Returns the downvotes of the post + */ + public async downvotes(): Promise { + this.loadDataIfNotExists(); + return this.$downvotes; + } + + /** + * The content of the post (markdown) + */ + public async content(): Promise { + this.loadDataIfNotExists(); + return this.$content; + } + + /** + * The date the post was created at. + */ + public async createdAt(): Promise { + this.loadDataIfNotExists(); + return this.$createdAt; + } + + /** + * The autor of the post. + */ + public async author(): Promise { + this.loadDataIfNotExists(); + return new User(this.$author); + } + + /** + * Deletes the post. + */ + public async delete(): Promise { + const query = await queryHelper.first({ + text: "DELETE FROM posts WHERE id = $1", + values: [this.id], + }); + } + + /** + * The type of vote the user performed on the post. + */ + public async userVote(userId: number): Promise { + const result = await queryHelper.first({ + text: "SELECT vote_type FROM votes WHERE user_id = $1 AND item_id = $2", + values: [userId, this.id], + }); + if (result) { + return result.vote_type; + } else { + return null; + } + } + + /** + * Loads the data from the database if needed. + */ + protected async loadData(): Promise { + let result: any; + if (this.row) { + result = this.row; + } else { + result = await queryHelper.first({ + text: "SELECT * FROM posts WHERE posts.id = $1", + values: [this.id], + }); + } + 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 new file mode 100644 index 0000000..bb680c4 --- /dev/null +++ b/src/lib/dataaccess/User.ts @@ -0,0 +1,137 @@ +import {DataObject} from "./DataObject"; +import {queryHelper} from "./index"; +import {Post} from "./Post"; + +export class User extends DataObject { + private $name: string; + private $handle: string; + private $email: string; + private $greenpoints: number; + private $joinedAt: string; + + /** + * The name of the user + */ + public async name(): Promise { + this.loadDataIfNotExists(); + 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. + */ + public async handle(): Promise { + this.loadDataIfNotExists(); + 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 + */ + public async email(): Promise { + this.loadDataIfNotExists(); + 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 + */ + public async greenpoints(): Promise { + this.loadDataIfNotExists(); + 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 + */ + public async joinedAt(): Promise { + this.loadDataIfNotExists(); + return new Date(this.$joinedAt); + } + + /** + * Returns all posts for a user. + */ + public async posts(): Promise { + const result = await queryHelper.all({ + text: "SELECT * FROM posts WHERE author = $1", + values: [this.id], + }); + const posts = []; + + for (const row of result) { + posts.push(new Post(row.id, row)); + } + return posts; + } + + /** + * Fetches the data for the user. + */ + protected async loadData(): Promise { + let result: any; + if (this.row) { + result = this.row; + } else { + result = await queryHelper.first({ + text: "SELECT * FROM users WHERE user.id = $1", + values: [this.id], + }); + } + if (result) { + this.$name = result.name; + this.$handle = result.handle; + this.$email = result.email; + this.$greenpoints = result.greenpoints; + this.$joinedAt = result.joined_at; + this.dataLoaded = true; + } + } +} diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts new file mode 100644 index 0000000..1d610ef --- /dev/null +++ b/src/lib/dataaccess/index.ts @@ -0,0 +1,66 @@ +import {Pool} from "pg"; +import globals from "../globals"; +import {QueryHelper} from "../QueryHelper"; +import {User} from "./User"; + +const config = globals.config; +const tableCreationFile = __dirname + "/../../sql/create-tables.sql"; +const tableUpdateFile = __dirname + "/../../sql/update-tables.sql"; + +const dbClient: Pool = new Pool({ + database: config.database.database, + host: config.database.host, + password: config.database.password, + port: config.database.port, + user: config.database.user, +}); +export const queryHelper = new QueryHelper(dbClient, tableCreationFile, tableUpdateFile); + +namespace dataaccess { + /** + * Initializes everything that needs to be initialized asynchronous. + */ + export async function init() { + await queryHelper.updateTableDefinitions(); + 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) { + const result = await this.queryHelper.first({ + text: "SELECT * FROM users WHERE users.handle = $1", + values: [userHandle], + }); + return new User(result.id, result); + } + + /** + * Enum representing the types of votes that can be performed on a post. + */ + export enum VoteType { + UPVOTE = "UPVOTE", + DOWNVOTE = "DOWNVOTE", + } + + /** + * Enum representing the types of request that can be created. + */ + export enum RequestType { + FRIENDREQUEST = "FRIENDREQUEST", + GROUPINVITE = "GROUPINVITE", + EVENTINVITE = "EVENTINVITE", + } +} + +export default dataaccess; diff --git a/src/lib/globals.ts b/src/lib/globals.ts index a64aaf0..a85ffea 100644 --- a/src/lib/globals.ts +++ b/src/lib/globals.ts @@ -1,7 +1,22 @@ +/** + * @author Trivernis + * @remarks + * + * Partly taken from {@link https://github.com/Trivernis/whooshy} + */ + import * as fsx from "fs-extra"; import * as yaml from "js-yaml"; import * as winston from "winston"; +const configPath = "config.yaml"; +const defaultConfig = __dirname + "/../default-config.yaml"; + +// ensure that the config exists by copying the default config. +if (!(fsx.pathExistsSync(configPath))) { + fsx.copySync(defaultConfig, configPath); +} + /** * Defines global variables to be used. */ diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index f5feb67..ff60fd3 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -1,100 +1,145 @@ type Query { - "returns the user object for a given user id" - getUser(userId: ID): User - "returns the post object for a post id" - getPost(postId: ID): Post - "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] - "find a user by user name or handle" - findUser(first: Int, offset: Int, name: String!, handle: String!): [User] + "returns the user object for a given user id" + getUser(userId: ID): User + + "returns the post object for a post id" + getPost(postId: ID): Post + + "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] + + "find a user by user name or handle" + findUser(first: Int, offset: Int, name: String!, handle: String!): [User] } type Mutation { - "Upvote/downvote a Post" - vote(postId: ID!, type: [VoteType!]!): Boolean - "Report the post" - report(postId: ID!): Boolean + "Upvote/downvote a Post" + vote(postId: ID!, type: [VoteType!]!): Boolean + + "Report the post" + report(postId: ID!): Boolean + + "send a request" + sendRequest(reciever: ID!, type: RequestType): Boolean + "lets you accept a request for a given request id" - acceptRequest(requestId: ID!): Boolean + acceptRequest(requestId: ID!): Boolean + + "lets you deny a request for a given request id" + denyRequest(requestId: ID!): Boolean + "send a message in a Chatroom" - sendMessage(chatId: ID!, content: String!): Boolean + sendMessage(chatId: ID!, content: String!): Boolean + + "create the post" + createPost(text: String, picture: String, tags: [String]): Boolean + + "delete the post for a given post id" + deletePost(postId: ID!): Boolean } "represents a single user account" type User { - "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" - getAllPosts(first: Int=10, offset: Int): [Post] - "creation date of the user account" - joinedDate: String! - "returns chats the user pinned" - pinnedChats: [ChatRoom] - "returns all friends of the user" - friends: [User] - "all request for groupChats/friends/events" - requests: [Request] + "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" + getAllPosts(first: Int=10, offset: Int): [Post] + + "creation date of the user account" + joinedDate: String! + + "returns chats the user pinned" + pinnedChats: [ChatRoom] + + "returns all friends of the user" + friends: [User] + + "all request for groupChats/friends/events" + requests: [Request] } "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 - "upvotes of the Post" - upvotes: Int! - "downvotes of the Post" - downvotes: Int! - "the user that is the author of the Post" - author: User! - "date the post was created" - creationDate: String! - "returns the type of vote the user performed on the post" - alreadyVoted: VoteType - "returns the tags of the post" - tags: [String] + "returns the path to the posts picture if it has one" + picture: String + + "returns the text of the post" + text: String + + "upvotes of the Post" + upvotes: Int! + + "downvotes of the Post" + downvotes: Int! + + "the user that is the author of the Post" + author: User! + + "date the post was created" + creationDate: String! + + "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" type Request { - "id of the request" - id: ID! - "type of the request" - requestType: RequestType! + "id of the request" + id: ID! + + "Id of the user who sended the request" + sender: User! + + "Id of the user who received the request" + receiver: User! + + "type of the request" + requestType: RequestType! } "represents a chatroom" type ChatRoom { - "the members of the chatroom" - members: [User!] - "return a specfic range of messages posted in the chat" - getMessages(first: Int, offset: Int): [String] - "id of the chat" - id: ID! + "the members of the chatroom" + members: [User!] + + "return a specfic range of messages posted in the chat" + getMessages(first: Int, offset: Int): [String] + + "id of the chat" + id: ID! } "represents the type of vote performed on a post" enum VoteType { - UPVOTE - DOWNVOTE + UPVOTE + DOWNVOTE } "represents the type of request that the user has received" enum RequestType { - FRIENDREQUEST - GROUPINVITE - EVENTINVITE + FRIENDREQUEST + GROUPINVITE + EVENTINVITE } diff --git a/src/public/stylesheets/sass/mixins.sass b/src/public/stylesheets/sass/mixins.sass new file mode 100644 index 0000000..14bca84 --- /dev/null +++ b/src/public/stylesheets/sass/mixins.sass @@ -0,0 +1,5 @@ +@mixin gridPosition($rowStart, $rowEnd, $columnStart, $columnEnd) + grid-row-start: $rowStart + grid-row-end: $rowEnd + grid-column-start: $columnStart + grid-column-end: $columnEnd diff --git a/src/public/stylesheets/sass/style.sass b/src/public/stylesheets/sass/style.sass index e69de29..5a86235 100644 --- a/src/public/stylesheets/sass/style.sass +++ b/src/public/stylesheets/sass/style.sass @@ -0,0 +1,82 @@ +@import "vars" +@import "mixins" + +body + font-family: Arial, serif + +button + border: 2px solid $cPrimary + margin-top: 0.125em + padding: 0.125em + background-color: $cPrimary + color: $cPrimarySurface + font-weight: bold + transition-duration: 0.25s + +button:hover + background-color: lighten($cPrimary, 10%) + cursor: pointer + +button:active + background-color: darken($cPrimary, 5%) + box-shadow: inset 0.25em 0.25em 0.1em rgba(0, 0, 0, 0.25) + +.stylebar + @include gridPosition(1, 2, 1, 4) + display: grid + grid-template: 100% /25% 50% 25% + background-color: $cPrimary + color: $cPrimarySurface + + h1 + @include gridPosition(1, 2, 1, 2) + text-align: center + margin: auto + +#content + grid-template: 7.5% 92.5% / 25% 50% 25% + display: grid + width: 100% + height: 100% + +#friendscontainer + @include gridPosition(2, 3, 1, 2) + background-color: $cPrimaryBackground + +#feedcontainer + @include gridPosition(2, 3, 2, 3) + background-color: $cSecondaryBackground + .postinput + margin: 0.5em + input + width: 100% + border-radius: 0.25em + border: 1px solid $cPrimary + padding: 0.125em + height: 2em + button.submitbutton + border-radius: 0.25em + height: 2em + + .feeditem + background-color: $cPrimaryBackground + min-height: 2em + margin: 0.5em + padding: 0.25em + border-radius: 0.25em + .itemhead + align-items: flex-start + + .title, .handle, .date + margin: 0.125em + .title + font-weight: bold + + .handle, .date + color: $cInactiveText + .handle a + text-decoration: none + color: $cInactiveText + font-style: normal + .handle a:hover + text-decoration: underline diff --git a/src/public/stylesheets/sass/vars.sass b/src/public/stylesheets/sass/vars.sass new file mode 100644 index 0000000..871a831 --- /dev/null +++ b/src/public/stylesheets/sass/vars.sass @@ -0,0 +1,5 @@ +$cPrimaryBackground: #fff +$cSecondaryBackground: #ddd +$cInactiveText: #555 +$cPrimary: #0d6b14 +$cPrimarySurface: #fff diff --git a/src/routes/home.ts b/src/routes/home.ts index d1015f7..1af3be0 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -1,10 +1,55 @@ import {Router} from "express"; +import {Server} from "socket.io"; +import Route from "../lib/Route"; -const router = Router(); +/** + * Class for the home route. + */ +class HomeRoute extends Route { + /** + * Constructor, creates new router. + */ + constructor() { + super(); + this.router = Router(); + this.configure(); + } -/* GET home page. */ -router.get("/", (req, res) => { - res.render("home"); -}); + /** + * Asynchronous init for socket.io. + * @param io - the io instance + */ + public async init(io: Server) { + this.io = io; + } -export default router; + /** + * Destroys the instance by dereferencing the router and resolver. + */ + public async destroy(): Promise { + this.router = null; + this.resolver = null; + } + + /** + * Returns the resolvers for the graphql api. + * @param req - the request object + * @param res - the response object + */ + public async resolver(req: any, res: any): Promise { + return { + // TODO: Define grapql resolvers + }; + } + + /** + * Configures the route. + */ + private configure() { + this.router.get("/", (req, res) => { + res.render("home"); + }); + } +} + +export default HomeRoute; diff --git a/src/routes/index.ts b/src/routes/index.ts index b8e9138..84cc7c5 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,21 +1,43 @@ +/** + * @author Trivernis + * @remarks + * + * Taken from {@link https://github.com/Trivernis/whooshy} + */ + import {Router} from "express"; import {Server} from "socket.io"; -import homeRouter from "./home"; +import HomeRoute from "./home"; + +const homeRoute = new HomeRoute(); +/** + * Namespace to manage the routes of the server. + * Allows easier assignments of graphql endpoints, socket.io connections and routers when + * used with {@link Route}. + */ namespace routes { export const router = Router(); - router.use("/", homeRouter); + router.use("/", homeRoute.router); + /** + * Asnyc function to create a graphql resolver that takes the request and response + * of express.js as arguments. + * @param request + * @param response + */ export const resolvers = async (request: any, response: any): Promise => { - return { - }; + return homeRoute.resolver(request, response); }; - // tslint:disable-next-line:no-empty - export const ioListeners = (io: Server) => { - + /** + * Assigns the io listeners or namespaces to the routes + * @param io + */ + export const ioListeners = async (io: Server) => { + await homeRoute.init(io); }; } diff --git a/src/sql/create-tables.sql b/src/sql/create-tables.sql index 0d02b2f..7c1c97a 100644 --- a/src/sql/create-tables.sql +++ b/src/sql/create-tables.sql @@ -20,7 +20,8 @@ CREATE TABLE IF NOT EXISTS posts ( CREATE TABLE IF NOT EXISTS votes ( user_id SERIAL REFERENCES users (id) ON DELETE CASCADE, - item_id BIGSERIAL REFERENCES posts (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 ( diff --git a/src/sql/update-tables.sql b/src/sql/update-tables.sql new file mode 100644 index 0000000..52ce2f6 --- /dev/null +++ b/src/sql/update-tables.sql @@ -0,0 +1,3 @@ +ALTER TABLE IF EXISTS votes + ADD COLUMN IF NOT EXISTS vote_type varchar(8) DEFAULT 'upvote', + ALTER COLUMN vote_type SET DEFAULT 'upvote'; diff --git a/src/views/home.pug b/src/views/home.pug deleted file mode 100644 index 8b7aebb..0000000 --- a/src/views/home.pug +++ /dev/null @@ -1,5 +0,0 @@ -html - head - title Greenvironment Network - body - h1 Greenvironment diff --git a/src/views/home/feed.pug b/src/views/home/feed.pug new file mode 100644 index 0000000..e3758e1 --- /dev/null +++ b/src/views/home/feed.pug @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..ed36fb7 --- /dev/null +++ b/src/views/home/friends.pug @@ -0,0 +1 @@ +div#friendscontainer diff --git a/src/views/home/index.pug b/src/views/home/index.pug new file mode 100644 index 0000000..0fc45ad --- /dev/null +++ b/src/views/home/index.pug @@ -0,0 +1,9 @@ +html + head + title Greenvironment Network + include ../includes/head + body + div#content + include stylebar + include feed + include friends diff --git a/src/views/home/stylebar.pug b/src/views/home/stylebar.pug new file mode 100644 index 0000000..c6c36e3 --- /dev/null +++ b/src/views/home/stylebar.pug @@ -0,0 +1,2 @@ +div.stylebar + h1 Greenvironment diff --git a/src/views/includes/head.pug b/src/views/includes/head.pug new file mode 100644 index 0000000..9e917e6 --- /dev/null +++ b/src/views/includes/head.pug @@ -0,0 +1 @@ +link(rel='stylesheet' href='stylesheets/style.css')