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

pull/1/head
Max_ES 5 years ago committed by Gitea
commit 9629e2de71

1
.gitignore vendored

@ -5,3 +5,4 @@ npm-debug.log
test/*.log test/*.log
dist dist
.idea .idea
config.yaml

@ -7,3 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- Connection to Postgres Database
- Graphql Schema
- default-config file and generation of config file on startup

@ -1 +1,11 @@
greenvironment-server # greenvironment-server
Server of the greenvironment social network.
## Install
You need to install a nodejs runtime to run the greenvironment server.
Then you need to install all requirements. To do so, open a terminal in the
greenvironment project folder and execute "npm i". You can build the project by
executing "gulp" in the terminal. To run the server you need
to execute "node ./dist".

@ -34,7 +34,12 @@ function compileSass() {
.pipe(dest('dist/public/stylesheets')); .pipe(dest('dist/public/stylesheets'));
} }
task('default', series(clearDist, compileTypescript, minifyJs, compileSass)); function moveRemaining() {
return src(['src/**/*', '!src/**/*.ts', '!src/**/*.sass', '!src/**/*.js'])
.pipe(dest('dist'));
}
task('default', series(clearDist, compileTypescript, minifyJs, compileSass, moveRemaining));
task('watch', () => { task('watch', () => {
watch('src/public/stylesheets/sass/**/*.sass', compileSass); watch('src/public/stylesheets/sass/**/*.sass', compileSass);
watch('**/*.js', minifyJs); watch('**/*.js', minifyJs);

13
package-lock.json generated

@ -137,6 +137,11 @@
"integrity": "sha512-UoCovaxbJIxagCvVfalfK7YaNhmxj3BQFRQ2RHQKLiu+9wNXhJnlbspsLHt/YQM99IaLUUFJNzCwzc6W0ypMeQ==", "integrity": "sha512-UoCovaxbJIxagCvVfalfK7YaNhmxj3BQFRQ2RHQKLiu+9wNXhJnlbspsLHt/YQM99IaLUUFJNzCwzc6W0ypMeQ==",
"dev": true "dev": true
}, },
"@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=="
},
"@types/mime": { "@types/mime": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
@ -370,7 +375,6 @@
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": { "requires": {
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
@ -1776,8 +1780,7 @@
"esprima": { "esprima": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
"dev": true
}, },
"esutils": { "esutils": {
"version": "2.0.3", "version": "2.0.3",
@ -3815,7 +3818,6 @@
"version": "3.13.1", "version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": { "requires": {
"argparse": "^1.0.7", "argparse": "^1.0.7",
"esprima": "^4.0.0" "esprima": "^4.0.0"
@ -5913,8 +5915,7 @@
"sprintf-js": { "sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
"dev": true
}, },
"sshpk": { "sshpk": {
"version": "1.16.1", "version": "1.16.1",

@ -40,6 +40,7 @@
"typescript": "^3.5.3" "typescript": "^3.5.3"
}, },
"dependencies": { "dependencies": {
"@types/js-yaml": "^3.12.1",
"@types/winston": "^2.4.4", "@types/winston": "^2.4.4",
"connect-pg-simple": "^6.0.0", "connect-pg-simple": "^6.0.0",
"cookie-parser": "^1.4.4", "cookie-parser": "^1.4.4",
@ -50,6 +51,7 @@
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"graphql": "^14.4.2", "graphql": "^14.4.2",
"graphql-import": "^0.7.1", "graphql-import": "^0.7.1",
"js-yaml": "^3.13.1",
"pg": "^7.12.1", "pg": "^7.12.1",
"socket.io": "^2.2.0", "socket.io": "^2.2.0",
"winston": "^3.2.1" "winston": "^3.2.1"

@ -1,16 +1,43 @@
import * as express from "express"; import * as express from "express";
import * as http from "http"; import * as http from "http";
import * as socketIo from "socket.io"; import * as socketIo from "socket.io";
import {DAO} from "./lib/DAO";
import globals from "./lib/globals";
class App { class App {
public app: express.Application; public app: express.Application;
public io: socketIo.Server; public io: socketIo.Server;
public server: http.Server; public server: http.Server;
public dao: DAO;
constructor() { constructor() {
this.app = express(); this.app = express();
this.server = new http.Server(this.app); this.server = new http.Server(this.app);
this.io = socketIo(this.server); this.io = socketIo(this.server);
this.dao = new DAO();
}
/**
* initializes everything that needs to be initialized asynchronous.
*/
public async init() {
await this.dao.init();
this.app.all("/", (req, res) => {
res.send("WIP!");
});
}
/**
* Starts the web server.
*/
public start() {
if (globals.config.server.port) {
globals.logger.info(`Starting server...`);
this.app.listen(globals.config.server.port);
globals.logger.info(`Server running on port ${globals.config.server.port}`);
} else {
globals.logger.error("No port specified in the config." +
"Please configure a port in the config.yaml.");
}
} }
} }

@ -0,0 +1,11 @@
# database connection info
database:
host:
port:
user:
password:
database:
# http server configuration
server:
port: 8080

@ -1,5 +1,18 @@
import * as fsx from "fs-extra";
import App from "./app"; import App from "./app";
const app = new 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();
})();
// TODO: init and start

@ -0,0 +1,27 @@
import {Pool} from "pg";
import globals from "./globals";
import {QueryHelper} from "./QueryHelper";
const config = globals.config;
const tableCreationFile = __dirname + "/../sql/create-tables.sql";
export class DAO {
private queryHelper: QueryHelper;
constructor() {
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,
});
this.queryHelper = new QueryHelper(dbClient, tableCreationFile);
}
/**
* Initializes everything that needs to be initialized asynchronous.
*/
public async init() {
await this.queryHelper.createTables();
}
}

@ -0,0 +1,128 @@
import * as fsx from "fs-extra";
import {Pool, PoolClient, QueryConfig, QueryResult} from "pg";
import globals from "./globals";
const logger = globals.logger;
export class SqlTransaction {
constructor(private client: PoolClient) {
}
/**
* Begins the transaction.
*/
public async begin() {
return await this.client.query("BEGIN");
}
/**
* Commits the transaction
*/
public async commit() {
return await this.client.query("COMMIT");
}
/**
* Rolls back the transaction
*/
public async rollback() {
return await this.client.query("ROLLBACK");
}
/**
* Executes a query inside the transaction.
* @param query
*/
public async query(query: QueryConfig) {
return await this.client.query(query);
}
/**
* Releases the client back to the pool.
*/
public async release() {
this.client.release();
}
}
export class QueryHelper {
private pool: Pool;
constructor(pgPool: Pool, private tableCreationFile?: string) {
this.pool = pgPool;
}
/**
* creates all tables needed if a filepath was given with the constructor
*/
public async createTables() {
if (this.tableCreationFile) {
logger.info("Creating nonexistent tables...");
const tableSql = await fsx.readFile(this.tableCreationFile, "utf-8");
await this.query({text: tableSql});
}
}
/**
* executes the sql query with values and returns all results.
* @param query
*/
public async all(query: QueryConfig): Promise<any[]> {
const result = await this.query(query);
return result.rows;
}
/**
* executes the sql query with values and returns the first result.
* @param query
*/
public async first(query: QueryConfig): Promise<any> {
const result = await this.query(query);
if (result.rows && result.rows.length > 0) {
return result.rows[0];
}
}
/**
* Creates a new Transaction to be uses with error handling.
*/
public async createTransaction() {
const client: PoolClient = await this.pool.connect();
return new SqlTransaction(client);
}
/**
* Queries the database with error handling.
* @param query - the sql and values to execute
*/
private async query(query: QueryConfig): Promise<QueryResult|{rows: any}> {
try {
return await this.pool.query(query);
} catch (err) {
logger.debug(`Error on query "${query}".`);
logger.error(`Sql query failed: ${err}`);
logger.verbose(err.stack);
return {
rows: null,
};
}
}
}
/**
* Returns the parameterized value sql for inserting
* @param columnCount
* @param rowCount
* @param [offset]
*/
export function buildSqlParameters(columnCount: number, rowCount: number, offset?: number): string {
let sql = "";
for (let i = 0; i < rowCount; i++) {
sql += "(";
for (let j = 0; j < columnCount; j++) {
sql += `$${(i * columnCount) + j + 1 + offset},`;
}
sql = sql.replace(/,$/, "") + "),";
}
return sql.replace(/,$/, "");
}

@ -0,0 +1,25 @@
import * as fsx from "fs-extra";
import * as yaml from "js-yaml";
import * as winston from "winston";
/**
* Defines global variables to be used.
*/
namespace globals {
export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8"));
export const logger = winston.createLogger({
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.colorize(),
winston.format.printf(({ level, message, label, timestamp }) => {
return `${timestamp} ${level}: ${message}`;
}),
),
}),
],
});
}
export default globals;

@ -0,0 +1,100 @@
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]
}
type Mutation {
"Upvote/downvote a Post"
vote(postId: ID!, type: [VoteType!]!): Boolean
"Report the post"
report(postId: ID!): Boolean
"lets you accept a request for a given request id"
acceptRequest(requestId: ID!): Boolean
"send a message in a Chatroom"
sendMessage(chatId: ID!, content: String!): 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]
}
"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]
}
"represents a request of any type"
type Request {
"id of the request"
id: ID!
"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!
}
"represents the type of vote performed on a post"
enum VoteType {
UPVOTE
DOWNVOTE
}
"represents the type of request that the user has received"
enum RequestType {
FRIENDREQUEST
GROUPINVITE
EVENTINVITE
}

@ -0,0 +1,57 @@
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
);
CREATE TABLE IF NOT EXISTS votes (
user_id SERIAL REFERENCES users (id) ON DELETE CASCADE,
item_id BIGSERIAL REFERENCES posts (id) ON DELETE CASCADE
);
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
);

@ -18,7 +18,10 @@
"no-console": { "no-console": {
"severity": "warning", "severity": "warning",
"options": ["debug", "info", "log", "time", "timeEnd", "trace"] "options": ["debug", "info", "log", "time", "timeEnd", "trace"]
} },
"no-namespace": false,
"no-internal-module": false,
"max-classes-per-file": false
}, },
"jsRules": { "jsRules": {
"max-line-length": { "max-line-length": {

Loading…
Cancel
Save