Merge branch 'develop' of Software_Engineering_I/greenvironment-server into master
commit
56614f6338
@ -1,8 +1,12 @@
|
|||||||
.nyc_output/
|
.nyc_output/
|
||||||
coverage/
|
coverage/
|
||||||
node_modules/
|
node_modules/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
test/*.log
|
test/*.log
|
||||||
dist
|
dist
|
||||||
.idea
|
.idea
|
||||||
config.yaml
|
config.yaml
|
||||||
|
sqz-force
|
||||||
|
greenvironment.db
|
||||||
|
logs
|
||||||
|
logs*
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Connection to Postgres Database
|
- Graphql Schema
|
||||||
- Graphql Schema
|
- default-config file and generation of config file on startup
|
||||||
- default-config file and generation of config file on startup
|
- DTOs
|
||||||
- DTOs
|
- Home Route
|
||||||
- Home Route
|
- session management
|
||||||
|
- Sequelize models and integration
|
||||||
|
- Sequelize-typescript integration
|
||||||
|
- error pages
|
||||||
|
- pagination for most list types
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
FROM node:current-alpine
|
FROM node:current-alpine
|
||||||
|
|
||||||
COPY . /home/node/green
|
COPY . /home/node/green
|
||||||
WORKDIR /home/node/green
|
WORKDIR /home/node/green
|
||||||
RUN npm install -g gulp
|
RUN npm install -g gulp
|
||||||
RUN npm install --save-dev
|
RUN npm install --save-dev
|
||||||
RUN npm rebuild node-sass
|
RUN npm rebuild node-sass
|
||||||
RUN gulp
|
RUN gulp
|
||||||
COPY . .
|
COPY . .
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
EXPOSE 5432
|
CMD ["npm" , "run"]
|
||||||
CMD ["npm" , "run"]
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
# greenvironment-server
|
# greenvironment-server
|
||||||
|
|
||||||
Server of the greenvironment social network.
|
Server of the greenvironment social network.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
You need to install a nodejs runtime to run the greenvironment server.
|
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
|
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
|
greenvironment project folder and execute "npm i". You can build the project by
|
||||||
executing "gulp" in the terminal. To run the server you need
|
executing "gulp" in the terminal. To run the server you need
|
||||||
to execute "node ./dist".
|
to execute "node ./dist".
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
greenvironment:
|
greenvironment:
|
||||||
build:
|
build: .
|
||||||
context: .
|
user: "root"
|
||||||
dockerfile: ./Dockerfile
|
working_dir: /home/node/green
|
||||||
user: "node"
|
environment:
|
||||||
working_dir: /home/node/green
|
- NODE_ENV=production
|
||||||
environment:
|
ports:
|
||||||
- NODE_ENV=production
|
- "8080:8080"
|
||||||
volumes:
|
command: "npm start"
|
||||||
- ./:/home/node/green
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
command: "npm start"
|
|
||||||
|
@ -1,47 +1,47 @@
|
|||||||
const {src, dest, watch, series, task} = require('gulp');
|
const {src, dest, watch, series, task} = require('gulp');
|
||||||
const sass = require('gulp-sass');
|
const sass = require('gulp-sass');
|
||||||
const ts = require('gulp-typescript');
|
const ts = require('gulp-typescript');
|
||||||
const minify = require('gulp-minify');
|
const minify = require('gulp-minify');
|
||||||
const del = require('delete');
|
const del = require('delete');
|
||||||
const gulp = require('gulp');
|
const gulp = require('gulp');
|
||||||
|
|
||||||
function clearDist(cb) {
|
function clearDist(cb) {
|
||||||
del('dist/*', cb);
|
del('dist/*', cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileTypescript() {
|
function compileTypescript() {
|
||||||
let tsProject = ts.createProject('tsconfig.json');
|
let tsProject = ts.createProject('tsconfig.json');
|
||||||
let tsResult = tsProject.src().pipe(tsProject());
|
let tsResult = tsProject.src().pipe(tsProject());
|
||||||
return tsResult
|
return tsResult
|
||||||
//.pipe(minify())
|
//.pipe(minify())
|
||||||
.pipe(dest('dist'));
|
.pipe(dest('dist'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function minifyJs() {
|
function minifyJs() {
|
||||||
return src('src/public/javascripts/**/*.js')
|
return src('src/public/javascripts/**/*.js')
|
||||||
.pipe(minify({
|
.pipe(minify({
|
||||||
ext: {
|
ext: {
|
||||||
src: '-debug.js',
|
src: '-debug.js',
|
||||||
min: '.js'
|
min: '.js'
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.pipe(dest('dist/public/javascripts'));
|
.pipe(dest('dist/public/javascripts'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileSass() {
|
function compileSass() {
|
||||||
return src('src/public/stylesheets/sass/**/style.sass')
|
return src('src/public/stylesheets/sass/**/style.sass')
|
||||||
.pipe(sass().on('error', sass.logError))
|
.pipe(sass().on('error', sass.logError))
|
||||||
.pipe(dest('dist/public/stylesheets'));
|
.pipe(dest('dist/public/stylesheets'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveRemaining() {
|
function moveRemaining() {
|
||||||
return src(['src/**/*', '!src/**/*.ts', '!src/**/*.sass', '!src/**/*.js'])
|
return src(['src/**/*', '!src/**/*.ts', '!src/**/*.sass', '!src/**/*.js'])
|
||||||
.pipe(dest('dist'));
|
.pipe(dest('dist'));
|
||||||
}
|
}
|
||||||
|
|
||||||
task('default', series(clearDist, compileTypescript, minifyJs, compileSass, moveRemaining));
|
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);
|
||||||
watch('**/*.ts', compileTypescript);
|
watch('**/*.ts', compileTypescript);
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,70 +1,79 @@
|
|||||||
{
|
{
|
||||||
"name": "greenvironment-server",
|
"name": "greenvironment-server",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Server for greenvironment network",
|
"description": "Server for greenvironment network",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build": "gulp",
|
"build": "gulp",
|
||||||
"start": "node ./dist/index.js"
|
"start": "node ./dist/index.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.trivernis.net/Software_Engineering_I/greenvironment-server.git"
|
"url": "https://git.trivernis.net/Software_Engineering_I/greenvironment-server.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"server",
|
"server",
|
||||||
"nodejs",
|
"nodejs",
|
||||||
"express"
|
"express"
|
||||||
],
|
],
|
||||||
"author": "SoftEngI",
|
"author": "SoftEngI",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/compression": "^1.0.1",
|
"@types/bluebird": "^3.5.27",
|
||||||
"@types/connect-pg-simple": "^4.2.0",
|
"@types/compression": "^1.0.1",
|
||||||
"@types/cookie-parser": "^1.4.2",
|
"@types/connect-pg-simple": "^4.2.0",
|
||||||
"@types/express": "^4.17.1",
|
"@types/cookie-parser": "^1.4.2",
|
||||||
"@types/express-graphql": "^0.8.0",
|
"@types/cors": "^2.8.6",
|
||||||
"@types/express-session": "^1.15.14",
|
"@types/express": "^4.17.1",
|
||||||
"@types/express-socket.io-session": "^1.3.2",
|
"@types/express-graphql": "^0.8.0",
|
||||||
"@types/fs-extra": "^8.0.0",
|
"@types/express-session": "^1.15.14",
|
||||||
"@types/graphql": "^14.2.3",
|
"@types/express-socket.io-session": "^1.3.2",
|
||||||
"@types/http-status": "^0.2.30",
|
"@types/fs-extra": "^8.0.0",
|
||||||
"@types/js-yaml": "^3.12.1",
|
"@types/graphql": "^14.2.3",
|
||||||
"@types/markdown-it": "0.0.9",
|
"@types/http-status": "^0.2.30",
|
||||||
"@types/node": "^12.7.8",
|
"@types/js-yaml": "^3.12.1",
|
||||||
"@types/pg": "^7.11.0",
|
"@types/markdown-it": "0.0.9",
|
||||||
"@types/socket.io": "^2.1.2",
|
"@types/node": "^12.7.12",
|
||||||
"@types/winston": "^2.4.4",
|
"@types/pg": "^7.11.0",
|
||||||
"delete": "^1.1.0",
|
"@types/sequelize": "^4.28.5",
|
||||||
"gulp": "^4.0.2",
|
"@types/socket.io": "^2.1.2",
|
||||||
"gulp-minify": "^3.1.0",
|
"@types/validator": "^10.11.3",
|
||||||
"gulp-sass": "^4.0.2",
|
"@types/winston": "^2.4.4",
|
||||||
"gulp-typescript": "^5.0.1",
|
"delete": "^1.1.0",
|
||||||
"ts-lint": "^4.5.1",
|
"gulp": "^4.0.2",
|
||||||
"tsc": "^1.20150623.0",
|
"gulp-minify": "^3.1.0",
|
||||||
"tslint": "^5.19.0",
|
"gulp-sass": "^4.0.2",
|
||||||
"typescript": "^3.5.3"
|
"gulp-typescript": "^5.0.1",
|
||||||
},
|
"ts-lint": "^4.5.1",
|
||||||
"dependencies": {
|
"tsc": "^1.20150623.0",
|
||||||
"compression": "^1.7.4",
|
"tslint": "^5.19.0",
|
||||||
"connect-pg-simple": "^6.0.1",
|
"typescript": "^3.5.3"
|
||||||
"cookie-parser": "^1.4.4",
|
},
|
||||||
"express": "^4.17.1",
|
"dependencies": {
|
||||||
"express-graphql": "^0.9.0",
|
"compression": "^1.7.4",
|
||||||
"express-session": "^1.16.2",
|
"connect-session-sequelize": "^6.0.0",
|
||||||
"express-socket.io-session": "^1.3.5",
|
"cookie-parser": "^1.4.4",
|
||||||
"fs-extra": "^8.1.0",
|
"cors": "^2.8.5",
|
||||||
"g": "^2.0.1",
|
"express": "^4.17.1",
|
||||||
"graphql": "^14.4.2",
|
"express-graphql": "^0.9.0",
|
||||||
"graphql-import": "^0.7.1",
|
"express-session": "^1.16.2",
|
||||||
"http-status": "^1.3.2",
|
"express-socket.io-session": "^1.3.5",
|
||||||
"js-yaml": "^3.13.1",
|
"fs-extra": "^8.1.0",
|
||||||
"markdown-it": "^10.0.0",
|
"graphql": "^14.4.2",
|
||||||
"markdown-it-emoji": "^1.4.0",
|
"graphql-import": "^0.7.1",
|
||||||
"pg": "^7.12.1",
|
"http-status": "^1.3.2",
|
||||||
"pug": "^2.0.4",
|
"js-yaml": "^3.13.1",
|
||||||
"socket.io": "^2.2.0",
|
"markdown-it": "^10.0.0",
|
||||||
"winston": "^3.2.1"
|
"markdown-it-emoji": "^1.4.0",
|
||||||
}
|
"pg": "^7.12.1",
|
||||||
}
|
"pug": "^2.0.4",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"sequelize": "^5.19.6",
|
||||||
|
"sequelize-typescript": "^1.0.0",
|
||||||
|
"socket.io": "^2.2.0",
|
||||||
|
"sqlite3": "^4.1.0",
|
||||||
|
"winston": "^3.2.1",
|
||||||
|
"winston-daily-rotate-file": "^4.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,97 +1,118 @@
|
|||||||
import * as compression from "compression";
|
import * as compression from "compression";
|
||||||
import connectPgSimple = require("connect-pg-simple");
|
import * as cookieParser from "cookie-parser";
|
||||||
import * as cookieParser from "cookie-parser";
|
import * as cors from "cors";
|
||||||
import * as express from "express";
|
import {Request, Response} from "express";
|
||||||
import * as graphqlHTTP from "express-graphql";
|
import * as express from "express";
|
||||||
import * as session from "express-session";
|
import * as graphqlHTTP from "express-graphql";
|
||||||
import sharedsession = require("express-socket.io-session");
|
import * as session from "express-session";
|
||||||
import {buildSchema} from "graphql";
|
import sharedsession = require("express-socket.io-session");
|
||||||
import {importSchema} from "graphql-import";
|
import * as fsx from "fs-extra";
|
||||||
import * as http from "http";
|
import {buildSchema} from "graphql";
|
||||||
import * as path from "path";
|
import {importSchema} from "graphql-import";
|
||||||
import * as socketIo from "socket.io";
|
import * as http from "http";
|
||||||
import {resolver} from "./graphql/resolvers";
|
import * as httpStatus from "http-status";
|
||||||
import dataaccess, {queryHelper} from "./lib/dataaccess";
|
import * as path from "path";
|
||||||
import globals from "./lib/globals";
|
import {Sequelize} from "sequelize-typescript";
|
||||||
import routes from "./routes";
|
import * as socketIo from "socket.io";
|
||||||
|
import {resolver} from "./graphql/resolvers";
|
||||||
const logger = globals.logger;
|
import dataaccess from "./lib/dataaccess";
|
||||||
|
import globals from "./lib/globals";
|
||||||
const PgSession = connectPgSimple(session);
|
import routes from "./routes";
|
||||||
|
|
||||||
class App {
|
const SequelizeStore = require("connect-session-sequelize")(session.Store);
|
||||||
public app: express.Application;
|
const logger = globals.logger;
|
||||||
public io: socketIo.Server;
|
|
||||||
public server: http.Server;
|
class App {
|
||||||
|
public app: express.Application;
|
||||||
constructor() {
|
public io: socketIo.Server;
|
||||||
this.app = express();
|
public server: http.Server;
|
||||||
this.server = new http.Server(this.app);
|
public readonly sequelize: Sequelize;
|
||||||
this.io = socketIo(this.server);
|
|
||||||
}
|
constructor() {
|
||||||
|
this.app = express();
|
||||||
/**
|
this.server = new http.Server(this.app);
|
||||||
* initializes everything that needs to be initialized asynchronous.
|
this.io = socketIo(this.server);
|
||||||
*/
|
this.sequelize = new Sequelize(globals.config.database.connectionUri );
|
||||||
public async init() {
|
}
|
||||||
await dataaccess.init();
|
|
||||||
await routes.ioListeners(this.io);
|
/**
|
||||||
|
* initializes everything that needs to be initialized asynchronous.
|
||||||
const appSession = session({
|
*/
|
||||||
cookie: {
|
public async init() {
|
||||||
maxAge: Number(globals.config.session.cookieMaxAge) || 604800000,
|
await dataaccess.init(this.sequelize);
|
||||||
secure: "auto",
|
|
||||||
},
|
const appSession = session({
|
||||||
resave: false,
|
cookie: {
|
||||||
saveUninitialized: false,
|
maxAge: Number(globals.config.session.cookieMaxAge) || 604800000,
|
||||||
secret: globals.config.session.secret,
|
secure: "auto",
|
||||||
store: new PgSession({
|
},
|
||||||
pool: dataaccess.pool,
|
resave: false,
|
||||||
tableName: "user_sessions",
|
saveUninitialized: false,
|
||||||
}),
|
secret: globals.config.session.secret,
|
||||||
});
|
store: new SequelizeStore({db: this.sequelize}),
|
||||||
|
});
|
||||||
this.io.use(sharedsession(appSession, {autoSave: true}));
|
|
||||||
|
const force = fsx.existsSync("sqz-force");
|
||||||
this.app.set("views", path.join(__dirname, "views"));
|
logger.info(`Sequelize Table force: ${force}`);
|
||||||
this.app.set("view engine", "pug");
|
await this.sequelize.sync({force, logging: (msg) => logger.silly(msg)});
|
||||||
this.app.set("trust proxy", 1);
|
await routes.ioListeners(this.io);
|
||||||
|
|
||||||
this.app.use(compression());
|
this.io.use(sharedsession(appSession, {autoSave: true}));
|
||||||
this.app.use(express.json());
|
|
||||||
this.app.use(express.urlencoded({extended: false}));
|
this.app.set("views", path.join(__dirname, "views"));
|
||||||
this.app.use(express.static(path.join(__dirname, "public")));
|
this.app.set("view engine", "pug");
|
||||||
this.app.use(cookieParser());
|
this.app.set("trust proxy", 1);
|
||||||
this.app.use(appSession);
|
|
||||||
this.app.use((req, res, next) => {
|
this.app.use(compression());
|
||||||
logger.verbose(`${req.method} ${req.url}`);
|
this.app.use(express.json());
|
||||||
next();
|
this.app.use(express.urlencoded({extended: false}));
|
||||||
});
|
this.app.use(express.static(path.join(__dirname, "public")));
|
||||||
this.app.use(routes.router);
|
this.app.use(cookieParser());
|
||||||
this.app.use("/graphql", graphqlHTTP((request, response) => {
|
this.app.use(appSession);
|
||||||
return {
|
if (globals.config.server.cors) {
|
||||||
// @ts-ignore all
|
this.app.use(cors());
|
||||||
context: {session: request.session},
|
}
|
||||||
graphiql: true,
|
this.app.use((req, res, next) => {
|
||||||
rootValue: resolver(request, response),
|
logger.verbose(`${req.method} ${req.url}`);
|
||||||
schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))),
|
next();
|
||||||
};
|
});
|
||||||
}));
|
this.app.use(routes.router);
|
||||||
}
|
this.app.use("/graphql", graphqlHTTP((request, response) => {
|
||||||
|
return {
|
||||||
/**
|
// @ts-ignore all
|
||||||
* Starts the web server.
|
context: {session: request.session},
|
||||||
*/
|
graphiql: true,
|
||||||
public start() {
|
rootValue: resolver(request, response),
|
||||||
if (globals.config.server.port) {
|
schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))),
|
||||||
logger.info(`Starting server...`);
|
};
|
||||||
this.app.listen(globals.config.server.port);
|
}));
|
||||||
logger.info(`Server running on port ${globals.config.server.port}`);
|
this.app.use((req: any, res: Response) => {
|
||||||
} else {
|
if (globals.config.frontend.angularIndex) {
|
||||||
logger.error("No port specified in the config." +
|
res.sendFile(path.join(__dirname, globals.config.frontend.angularIndex));
|
||||||
"Please configure a port in the config.yaml.");
|
} else {
|
||||||
}
|
res.status(httpStatus.NOT_FOUND);
|
||||||
}
|
res.render("errors/404.pug", {url: req.url});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
export default App;
|
this.app.use((err, req: Request, res: Response) => {
|
||||||
|
res.status(httpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
res.render("errors/500.pug");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the web server.
|
||||||
|
*/
|
||||||
|
public start() {
|
||||||
|
if (globals.config.server.port) {
|
||||||
|
logger.info(`Starting server...`);
|
||||||
|
this.app.listen(globals.config.server.port);
|
||||||
|
logger.info(`Server running on port ${globals.config.server.port}`);
|
||||||
|
} else {
|
||||||
|
logger.error("No port specified in the config." +
|
||||||
|
"Please configure a port in the config.yaml.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
@ -1,236 +1,390 @@
|
|||||||
type Query {
|
type Query {
|
||||||
"returns the user object for a given user id or a handle (only one required)"
|
"returns the user object for a given user id or a handle (only one required)"
|
||||||
getUser(userId: ID, handle: String): User
|
getUser(userId: ID, handle: String): User
|
||||||
|
|
||||||
"returns the logged in user"
|
"returns the logged in user"
|
||||||
getSelf: Profile
|
getSelf: Profile
|
||||||
|
|
||||||
"returns the post object for a post id"
|
"returns the post object for a post id"
|
||||||
getPost(postId: ID!): Post
|
getPost(postId: ID!): Post
|
||||||
|
|
||||||
"returns the chat object for a chat id"
|
"returns the chat object for a chat id"
|
||||||
getChat(chatId: ID!): ChatRoom
|
getChat(chatId: ID!): ChatRoom
|
||||||
|
|
||||||
"find a post by the posted date or content"
|
"return shte group object for its id"
|
||||||
findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post]
|
getGroup(groupId: ID!): Group
|
||||||
|
|
||||||
"find a user by user name or handle"
|
"returns the request object for its id"
|
||||||
findUser(first: Int, offset: Int, name: String!, handle: String!): [User]
|
getRequest(requestId: ID!): Request
|
||||||
}
|
|
||||||
|
"find a post by the posted date or content"
|
||||||
type Mutation {
|
findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post]
|
||||||
"Accepts the usage of cookies."
|
|
||||||
acceptCookies: Boolean
|
"find a user by user name or handle"
|
||||||
|
findUser(first: Int, offset: Int, name: String, handle: String): [User]
|
||||||
"Login of the user. The passwordHash should be a sha512 hash of the password."
|
|
||||||
login(email: String, passwordHash: String): Profile
|
"returns the post filtered by the sort type with pagination."
|
||||||
|
getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post]
|
||||||
"Registers the user."
|
}
|
||||||
register(username: String, email: String, passwordHash: String): Profile
|
|
||||||
|
type Mutation {
|
||||||
"Logout of the user."
|
"Accepts the usage of cookies."
|
||||||
logout: Boolean
|
acceptCookies: Boolean
|
||||||
|
|
||||||
"Upvote/downvote a Post"
|
"Login of the user. The passwordHash should be a sha512 hash of the password."
|
||||||
vote(postId: ID!, type: VoteType!): VoteType
|
login(email: String, passwordHash: String): Profile
|
||||||
|
|
||||||
"Report the post"
|
"Registers the user."
|
||||||
report(postId: ID!): Boolean
|
register(username: String, email: String, passwordHash: String): Profile
|
||||||
|
|
||||||
"send a request"
|
"Logout of the user."
|
||||||
sendRequest(receiver: ID!, type: RequestType): Request
|
logout: Boolean
|
||||||
|
|
||||||
"lets you accept a request for a given request id"
|
"Upvote/downvote a Post"
|
||||||
acceptRequest(sender: ID!, type: RequestType): Boolean
|
vote(postId: ID!, type: VoteType!): VoteType
|
||||||
|
|
||||||
"lets you deny a request for a given request id"
|
"Report the post"
|
||||||
denyRequest(requestId: ID!): Boolean
|
report(postId: ID!): Boolean
|
||||||
|
|
||||||
"send a message in a Chatroom"
|
"send a request"
|
||||||
sendMessage(chatId: ID!, content: String!): ChatMessage
|
sendRequest(receiver: ID!, type: RequestType): Request
|
||||||
|
|
||||||
"create the post"
|
"lets you accept a request for a given request id"
|
||||||
createPost(content: String!): Post
|
acceptRequest(sender: ID!, type: RequestType): Boolean
|
||||||
|
|
||||||
"delete the post for a given post id"
|
"lets you deny a request for a given request id"
|
||||||
deletePost(postId: ID!): Boolean
|
denyRequest(requestId: ID!): Boolean
|
||||||
|
|
||||||
"Creates a chat between the user (and optional an other user)"
|
"removes a friend"
|
||||||
createChat(members: [ID!]): ChatRoom
|
removeFriend(friendId: ID!): Boolean
|
||||||
}
|
|
||||||
|
"send a message in a Chatroom"
|
||||||
interface UserData {
|
sendMessage(chatId: ID!, content: String!): ChatMessage
|
||||||
"url for the Profile picture of the User"
|
|
||||||
profilePicture: String
|
"create the post"
|
||||||
|
createPost(content: String!): Post!
|
||||||
"name of the User"
|
|
||||||
name: String!
|
"delete the post for a given post id"
|
||||||
|
deletePost(postId: ID!): Boolean!
|
||||||
"unique identifier name from the User"
|
|
||||||
handle: String!
|
"Creates a chat between the user (and optional an other user)"
|
||||||
|
createChat(members: [ID!]): ChatRoom!
|
||||||
"Id of the User"
|
|
||||||
id: ID!
|
"Creates a new group with a given name and additional members"
|
||||||
|
createGroup(name: String!, members: [ID!]): Group!
|
||||||
"the total number of posts the user posted"
|
|
||||||
numberOfPosts: Int
|
"Joins a group with the given id"
|
||||||
|
joinGroup(id: ID!): Group
|
||||||
"returns a given number of posts of a user"
|
|
||||||
posts(first: Int=10, offset: Int): [Post]
|
"leaves the group with the given id"
|
||||||
|
leaveGroup(id: ID!): Group
|
||||||
"creation date of the user account"
|
|
||||||
joinedAt: String!
|
"adds an admin to the group"
|
||||||
|
addGroupAdmin(groupId: ID!, userId: ID!): Group
|
||||||
"all friends of the user"
|
|
||||||
friends: [User]
|
"removes an admin from the group"
|
||||||
}
|
removeGroupAdmin(groupId: ID!, userId: ID!): Group
|
||||||
|
|
||||||
"represents a single user account"
|
"Creates a new event with a epoch due date on a group."
|
||||||
type User implements UserData{
|
createEvent(name: String, dueDate: String, groupId: ID!): Event
|
||||||
"url for the Profile picture of the User"
|
|
||||||
profilePicture: String
|
"Joins a event."
|
||||||
|
joinEvent(eventId: ID!): Event
|
||||||
"name of the User"
|
|
||||||
name: String!
|
"Leaves a event."
|
||||||
|
leaveEvent(eventId: ID!): Event
|
||||||
"unique identifier name from the User"
|
}
|
||||||
handle: String!
|
|
||||||
|
interface UserData {
|
||||||
"Id of the User"
|
"url for the Profile picture of the User"
|
||||||
id: ID!
|
profilePicture: String
|
||||||
|
|
||||||
"the total number of posts the user posted"
|
"name of the User"
|
||||||
numberOfPosts: Int
|
name: String!
|
||||||
|
|
||||||
"returns a given number of posts of a user"
|
"unique identifier name from the User"
|
||||||
posts(first: Int=10, offset: Int): [Post]
|
handle: String!
|
||||||
|
|
||||||
"creation date of the user account"
|
"Id of the User"
|
||||||
joinedAt: String!
|
id: ID!
|
||||||
|
|
||||||
"all friends of the user"
|
"DEPRECATED! the total number of posts the user posted"
|
||||||
friends: [User]
|
numberOfPosts: Int!
|
||||||
}
|
|
||||||
|
"the number of posts the user has created"
|
||||||
type Profile implements UserData {
|
postCount: Int!
|
||||||
"url for the Profile picture of the User"
|
|
||||||
profilePicture: String
|
"returns a given number of posts of a user"
|
||||||
|
posts(first: Int=10, offset: Int=0): [Post]
|
||||||
"name of the User"
|
|
||||||
name: String!
|
"creation date of the user account"
|
||||||
|
joinedAt: String!
|
||||||
"returns the chatrooms the user joined."
|
|
||||||
chats(first: Int=10, offset: Int): [ChatRoom]
|
"all friends of the user"
|
||||||
|
friends(first: Int=10, offset: Int=0): [User]
|
||||||
"unique identifier name from the User"
|
|
||||||
handle: String!
|
"The number of friends the user has"
|
||||||
|
friendCount: Int!
|
||||||
"Id of the User"
|
|
||||||
id: ID!
|
"The groups the user has joined"
|
||||||
|
groups(first: Int=10, offset: Int=0): [Group]
|
||||||
"the total number of posts the user posted"
|
|
||||||
numberOfPosts: Int
|
"The numbef of groups the user has joined"
|
||||||
|
groupCount: Int!
|
||||||
"returns a given number of posts of a user"
|
|
||||||
posts(first: Int=10, offset: Int): [Post]
|
"the points of the user"
|
||||||
|
points: Int!
|
||||||
"creation date of the user account"
|
|
||||||
joinedAt: String!
|
"the levels of the user depending on the points"
|
||||||
|
level: Int!
|
||||||
"all friends of the user"
|
}
|
||||||
friends: [User]
|
|
||||||
|
"represents a single user account"
|
||||||
"all sent request for groupChats/friends/events"
|
type User implements UserData{
|
||||||
sentRequests: [Request]
|
"url for the Profile picture of the User"
|
||||||
|
profilePicture: String
|
||||||
"all received request for groupChats/friends/events"
|
|
||||||
receivedRequests: [Request]
|
"name of the User"
|
||||||
}
|
name: String!
|
||||||
|
|
||||||
"represents a single user post"
|
"unique identifier name from the User"
|
||||||
type Post {
|
handle: String!
|
||||||
|
|
||||||
"The id of the post."
|
"Id of the User"
|
||||||
id: ID!
|
id: ID!
|
||||||
|
|
||||||
"the text of the post"
|
"the total number of posts the user posted"
|
||||||
content: String
|
numberOfPosts: Int!
|
||||||
|
|
||||||
"the content of the post rendered by markdown-it"
|
"returns a given number of posts of a user"
|
||||||
htmlContent: String
|
posts(first: Int=10, offset: Int): [Post]
|
||||||
|
|
||||||
"upvotes of the Post"
|
"the number of posts the user has created"
|
||||||
upvotes: Int!
|
postCount: Int!
|
||||||
|
|
||||||
"downvotes of the Post"
|
"creation date of the user account"
|
||||||
downvotes: Int!
|
joinedAt: String!
|
||||||
|
|
||||||
"the user that is the author of the Post"
|
"all friends of the user"
|
||||||
author: User!
|
friends(first: Int=10, offset: Int=0): [User]
|
||||||
|
|
||||||
"date the post was created"
|
"The number of friends the user has"
|
||||||
createdAt: String!
|
friendCount: Int!
|
||||||
|
|
||||||
"the type of vote the user performed on the post"
|
"the points of the user"
|
||||||
userVote: VoteType
|
points: Int!
|
||||||
}
|
|
||||||
|
"the groups the user has joined"
|
||||||
"represents a request of any type"
|
groups(first: Int=10, offset: Int=0): [Group]
|
||||||
type Request {
|
|
||||||
"Id of the user who sended the request"
|
"The numbef of groups the user has joined"
|
||||||
sender: User!
|
groupCount: Int!
|
||||||
|
|
||||||
"Id of the user who received the request"
|
"the levels of the user depending on the points"
|
||||||
receiver: User!
|
level: Int!
|
||||||
|
}
|
||||||
"type of the request"
|
|
||||||
type: RequestType!
|
type Profile implements UserData {
|
||||||
}
|
"url for the Profile picture of the User"
|
||||||
|
profilePicture: String
|
||||||
"represents a chatroom"
|
|
||||||
type ChatRoom {
|
"name of the User"
|
||||||
"the socket.io namespace for the chatroom"
|
name: String!
|
||||||
namespace: String
|
|
||||||
|
"the email of the user"
|
||||||
"the members of the chatroom"
|
email: String!
|
||||||
members: [User!]
|
|
||||||
|
"returns the chatrooms the user joined."
|
||||||
"return a specfic range of messages posted in the chat"
|
chats(first: Int=10, offset: Int): [ChatRoom]
|
||||||
messages(first: Int = 10, offset: Int, containing: String): [ChatMessage]!
|
|
||||||
|
"the count of the users chats"
|
||||||
"id of the chat"
|
chatCount: Int!
|
||||||
id: ID!
|
|
||||||
}
|
"unique identifier name from the User"
|
||||||
|
handle: String!
|
||||||
type ChatMessage {
|
|
||||||
"The author of the chat message."
|
"Id of the User"
|
||||||
author: User!
|
id: ID!
|
||||||
|
|
||||||
"The chatroom the message was posted in"
|
"the total number of posts the user posted"
|
||||||
chat: ChatRoom!
|
numberOfPosts: Int!
|
||||||
|
|
||||||
"The timestamp when the message was posted (epoch)."
|
"the number of posts the user has created"
|
||||||
createdAt: String!
|
postCount: Int!
|
||||||
|
|
||||||
"The content of the message."
|
"returns a given number of posts of a user"
|
||||||
content: String!
|
posts(first: Int=10, offset: Int): [Post!]!
|
||||||
|
|
||||||
"The content of the message rendered by markdown-it."
|
"creation date of the user account"
|
||||||
htmlContent: String
|
joinedAt: String!
|
||||||
}
|
|
||||||
|
"all friends of the user"
|
||||||
"represents the type of vote performed on a post"
|
friends(first: Int=10, offset: Int=0): [User!]!
|
||||||
enum VoteType {
|
|
||||||
UPVOTE
|
"The number of friends the user has"
|
||||||
DOWNVOTE
|
friendCount: Int!
|
||||||
}
|
|
||||||
|
"all sent request for groupChats/friends/events"
|
||||||
"""
|
sentRequests: [Request!]!
|
||||||
represents the type of request that the user has received
|
|
||||||
Currently on Friend Requests are implemented.
|
"all received request for groupChats/friends/events"
|
||||||
"""
|
receivedRequests: [Request!]!
|
||||||
enum RequestType {
|
|
||||||
FRIENDREQUEST
|
"all groups the user is an admin of"
|
||||||
GROUPINVITE
|
administratedGroups: [Group!]!
|
||||||
EVENTINVITE
|
|
||||||
}
|
"all groups the user has created"
|
||||||
|
createdGroups: [Group!]!
|
||||||
|
|
||||||
|
"all groups the user has joined"
|
||||||
|
groups(first: Int=10, offset: Int=0): [Group!]!
|
||||||
|
|
||||||
|
"The numbef of groups the user has joined"
|
||||||
|
groupCount: Int!
|
||||||
|
|
||||||
|
"the points of the user"
|
||||||
|
points: Int!
|
||||||
|
|
||||||
|
"the levels of the user depending on the points"
|
||||||
|
level: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
"represents a single user post"
|
||||||
|
type Post {
|
||||||
|
|
||||||
|
"The id of the post."
|
||||||
|
id: ID!
|
||||||
|
|
||||||
|
"the text of the post"
|
||||||
|
content: String
|
||||||
|
|
||||||
|
"the content of the post rendered by markdown-it"
|
||||||
|
htmlContent: 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"
|
||||||
|
createdAt: String!
|
||||||
|
|
||||||
|
"the type of vote the user performed on the post"
|
||||||
|
userVote: VoteType
|
||||||
|
}
|
||||||
|
|
||||||
|
"represents a request of any type"
|
||||||
|
type Request {
|
||||||
|
|
||||||
|
"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"
|
||||||
|
type: RequestType!
|
||||||
|
}
|
||||||
|
|
||||||
|
"represents a chatroom"
|
||||||
|
type ChatRoom {
|
||||||
|
"the socket.io namespace for the chatroom"
|
||||||
|
namespace: String
|
||||||
|
|
||||||
|
"the members of the chatroom"
|
||||||
|
members(first: Int=10, offset: Int=0): [User!]
|
||||||
|
|
||||||
|
"return a specfic range of messages posted in the chat"
|
||||||
|
messages(first: Int = 10, offset: Int, containing: String): [ChatMessage]!
|
||||||
|
|
||||||
|
"id of the chat"
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatMessage {
|
||||||
|
"Id of the chat message"
|
||||||
|
id: ID!
|
||||||
|
|
||||||
|
"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)."
|
||||||
|
createdAt: String!
|
||||||
|
|
||||||
|
"The content of the message."
|
||||||
|
content: String!
|
||||||
|
|
||||||
|
"The content of the message rendered by markdown-it."
|
||||||
|
htmlContent: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Group {
|
||||||
|
"ID of the group"
|
||||||
|
id: ID!
|
||||||
|
|
||||||
|
"name of the group"
|
||||||
|
name: String!
|
||||||
|
|
||||||
|
"the creator of the group"
|
||||||
|
creator: User
|
||||||
|
|
||||||
|
"all admins of the group"
|
||||||
|
admins(first: Int=10, offset: Int=0): [User]!
|
||||||
|
|
||||||
|
"the members of the group with pagination"
|
||||||
|
members(first: Int = 10, offset: Int = 0): [User]!
|
||||||
|
|
||||||
|
"the groups chat"
|
||||||
|
chat: ChatRoom
|
||||||
|
|
||||||
|
"the events of the group"
|
||||||
|
events(first: Int=10, offset: Int=0): [Event!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event {
|
||||||
|
"ID of the event"
|
||||||
|
id: ID!
|
||||||
|
|
||||||
|
"Name of the event"
|
||||||
|
name: String!
|
||||||
|
|
||||||
|
"The date of the event."
|
||||||
|
dueDate: String!
|
||||||
|
|
||||||
|
"The group the event belongs to."
|
||||||
|
group: Group!
|
||||||
|
|
||||||
|
"The participants of the event."
|
||||||
|
participants(first: Int=10, offset: Int=0): [User!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
"represents the type of vote performed on a post"
|
||||||
|
enum VoteType {
|
||||||
|
UPVOTE
|
||||||
|
DOWNVOTE
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
represents the type of request that the user has received
|
||||||
|
Currently on Friend Requests are implemented.
|
||||||
|
"""
|
||||||
|
enum RequestType {
|
||||||
|
FRIENDREQUEST
|
||||||
|
GROUPINVITE
|
||||||
|
EVENTINVITE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SortType {
|
||||||
|
TOP
|
||||||
|
NEW
|
||||||
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import App from "./app";
|
import App from "./app";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* async main function wrapper.
|
* async main function wrapper.
|
||||||
*/
|
*/
|
||||||
(async () => {
|
(async () => {
|
||||||
const app = new App();
|
const app = new App();
|
||||||
await app.init();
|
await app.init();
|
||||||
app.start();
|
app.start();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
export enum InternalEvents {
|
/**
|
||||||
CHATCREATE = "chatCreate",
|
* Events that are emitted and processsed internally on the server
|
||||||
CHATMESSAGE = "chatMessage",
|
*/
|
||||||
GQLCHATMESSAGE = "graphqlChatMessage",
|
export enum InternalEvents {
|
||||||
REQUESTCREATE = "requestCreate",
|
CHATCREATE = "chatCreate",
|
||||||
POSTCREATE = "postCreate",
|
CHATMESSAGE = "chatMessage",
|
||||||
GQLPOSTCREATE = "graphqlPostCreate",
|
GQLCHATMESSAGE = "graphqlChatMessage",
|
||||||
}
|
REQUESTCREATE = "requestCreate",
|
||||||
|
POSTCREATE = "postCreate",
|
||||||
|
GQLPOSTCREATE = "graphqlPostCreate",
|
||||||
|
}
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
import * as crypto from "crypto";
|
|
||||||
import {EventEmitter} from "events";
|
|
||||||
|
|
||||||
export class MemoryCache extends EventEmitter {
|
|
||||||
private cacheItems: any = {};
|
|
||||||
private cacheExpires: any = {};
|
|
||||||
private expireCheck: NodeJS.Timeout;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates interval function.
|
|
||||||
* @param ttl
|
|
||||||
*/
|
|
||||||
constructor(private ttl: number = 500) {
|
|
||||||
super();
|
|
||||||
this.expireCheck = setInterval(() => this.checkExpires(), ttl / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a md5 hash of the given key.
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
public hashKey(key: string): string {
|
|
||||||
const hash = crypto.createHash("sha1");
|
|
||||||
const data = hash.update(key, "utf8");
|
|
||||||
return data.digest("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets an entry.
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
public set(key: string, value: any) {
|
|
||||||
this.cacheItems[key] = value;
|
|
||||||
this.cacheExpires[key] = Date.now() + this.ttl;
|
|
||||||
this.emit("set", key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the entry stored with the given key.
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
public get(key: string) {
|
|
||||||
if (this.cacheItems.hasOwnProperty(key)) {
|
|
||||||
this.emit("hit", key, this.cacheItems[key]);
|
|
||||||
return this.cacheItems[key];
|
|
||||||
} else {
|
|
||||||
this.emit("miss", key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a cache item.
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
public delete(key: string) {
|
|
||||||
this.emit("delete", key);
|
|
||||||
delete this.cacheItems[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks expires and clears items that are over the expire value.
|
|
||||||
*/
|
|
||||||
private checkExpires() {
|
|
||||||
for (const [key, value] of Object.entries(this.cacheExpires)) {
|
|
||||||
if (value < Date.now()) {
|
|
||||||
this.emit("delete", key);
|
|
||||||
delete this.cacheItems[key];
|
|
||||||
delete this.cacheExpires[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,212 +0,0 @@
|
|||||||
/**
|
|
||||||
* @author Trivernis
|
|
||||||
* @remarks
|
|
||||||
*
|
|
||||||
* Taken from {@link https://github.com/Trivernis/whooshy}
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as fsx from "fs-extra";
|
|
||||||
import {Pool, PoolClient, QueryConfig, QueryResult} from "pg";
|
|
||||||
import globals from "./globals";
|
|
||||||
|
|
||||||
const logger = globals.logger;
|
|
||||||
|
|
||||||
export interface IAdvancedQueryConfig extends QueryConfig {
|
|
||||||
cache?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transaction class to wrap SQL transactions.
|
|
||||||
*/
|
|
||||||
export class SqlTransaction {
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
* @param client
|
|
||||||
*/
|
|
||||||
constructor(private client: PoolClient) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Begins the transaction.
|
|
||||||
*/
|
|
||||||
public async begin() {
|
|
||||||
return await this.client.query("BEGIN");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Commits the transaction
|
|
||||||
*/
|
|
||||||
public async commit() {
|
|
||||||
return await this.client.query("COMMIT");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rolls back the transaction
|
|
||||||
*/
|
|
||||||
public async rollback() {
|
|
||||||
return await this.client.query("ROLLBACK");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes a query inside the transaction.
|
|
||||||
* @param query
|
|
||||||
*/
|
|
||||||
public async query(query: QueryConfig) {
|
|
||||||
return await this.client.query(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Releases the client back to the pool.
|
|
||||||
*/
|
|
||||||
public release() {
|
|
||||||
this.client.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query helper for easyer fetching of a specific row count.
|
|
||||||
*/
|
|
||||||
export class QueryHelper {
|
|
||||||
private pool: Pool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
* @param pgPool
|
|
||||||
* @param [tableCreationFile]
|
|
||||||
* @param [tableUpdateFile]
|
|
||||||
*/
|
|
||||||
constructor(pgPool: Pool, private tableCreationFile?: string, private tableUpdateFile?: string) {
|
|
||||||
this.pool = pgPool;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Async init function
|
|
||||||
*/
|
|
||||||
public async init() {
|
|
||||||
await this.pool.connect();
|
|
||||||
await this.createTables();
|
|
||||||
await this.updateTableDefinitions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates all tables needed if a filepath was given with the constructor
|
|
||||||
*/
|
|
||||||
public async createTables() {
|
|
||||||
if (this.tableCreationFile) {
|
|
||||||
logger.info("Creating nonexistent tables...");
|
|
||||||
const tableSql = await fsx.readFile(this.tableCreationFile, "utf-8");
|
|
||||||
const trans = await this.createTransaction();
|
|
||||||
await trans.begin();
|
|
||||||
try {
|
|
||||||
await trans.query({text: tableSql});
|
|
||||||
await trans.commit();
|
|
||||||
} catch (err) {
|
|
||||||
globals.logger.error(`Error on table creation ${err.message}`);
|
|
||||||
globals.logger.debug(err.stack);
|
|
||||||
await trans.rollback();
|
|
||||||
} finally {
|
|
||||||
trans.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the definition of the tables if the table update file was passed in the constructor
|
|
||||||
*/
|
|
||||||
public async updateTableDefinitions() {
|
|
||||||
if (this.tableUpdateFile) {
|
|
||||||
logger.info("Updating table definitions...");
|
|
||||||
const tableSql = await fsx.readFile(this.tableUpdateFile, "utf-8");
|
|
||||||
const trans = await this.createTransaction();
|
|
||||||
await trans.begin();
|
|
||||||
try {
|
|
||||||
await trans.query({text: tableSql});
|
|
||||||
await trans.commit();
|
|
||||||
} catch (err) {
|
|
||||||
globals.logger.error(`Error on table update ${err.message}`);
|
|
||||||
globals.logger.debug(err.stack);
|
|
||||||
await trans.rollback();
|
|
||||||
} finally {
|
|
||||||
trans.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* executes the sql query with values and returns all results.
|
|
||||||
* @param query
|
|
||||||
*/
|
|
||||||
public async all(query: IAdvancedQueryConfig): Promise<any[]> {
|
|
||||||
const result = await this.query(query);
|
|
||||||
return result.rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* executes the sql query with values and returns the first result.
|
|
||||||
* @param query
|
|
||||||
*/
|
|
||||||
public async first(query: IAdvancedQueryConfig): Promise<any> {
|
|
||||||
const result = await this.query(query);
|
|
||||||
if (result.rows && result.rows.length > 0) {
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new Transaction to be uses with error handling.
|
|
||||||
*/
|
|
||||||
public async createTransaction() {
|
|
||||||
const client: PoolClient = await this.pool.connect();
|
|
||||||
return new SqlTransaction(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries the database with error handling.
|
|
||||||
* @param query - the sql and values to execute
|
|
||||||
*/
|
|
||||||
private async query(query: IAdvancedQueryConfig): Promise<QueryResult|{rows: any}> {
|
|
||||||
try {
|
|
||||||
query.text = query.text.replace(/[\r\n]/g, " ");
|
|
||||||
globals.logger.silly(`Executing sql '${JSON.stringify(query)}'`);
|
|
||||||
|
|
||||||
if (query.cache) {
|
|
||||||
const key = globals.cache.hashKey(JSON.stringify(query));
|
|
||||||
const cacheResult = globals.cache.get(key);
|
|
||||||
if (cacheResult) {
|
|
||||||
return cacheResult;
|
|
||||||
} else {
|
|
||||||
const result = await this.pool.query(query);
|
|
||||||
globals.cache.set(key, result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return await this.pool.query(query);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.debug(`Error on query "${JSON.stringify(query)}".`);
|
|
||||||
logger.error(`Sql query failed: ${err}`);
|
|
||||||
logger.verbose(err.stack);
|
|
||||||
return {
|
|
||||||
rows: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the parameterized value sql for inserting
|
|
||||||
* @param columnCount
|
|
||||||
* @param rowCount
|
|
||||||
* @param [offset]
|
|
||||||
*/
|
|
||||||
export function buildSqlParameters(columnCount: number, rowCount: number, offset?: number): string {
|
|
||||||
let sql = "";
|
|
||||||
for (let i = 0; i < rowCount; i++) {
|
|
||||||
sql += "(";
|
|
||||||
for (let j = 0; j < columnCount; j++) {
|
|
||||||
sql += `$${(i * columnCount) + j + 1 + offset},`;
|
|
||||||
}
|
|
||||||
sql = sql.replace(/,$/, "") + "),";
|
|
||||||
}
|
|
||||||
return sql.replace(/,$/, "");
|
|
||||||
}
|
|
@ -1,26 +1,20 @@
|
|||||||
/**
|
import {Router} from "express";
|
||||||
* @author Trivernis
|
import {Namespace, Server} from "socket.io";
|
||||||
* @remarks
|
|
||||||
*
|
/**
|
||||||
* Taken from {@link https://github.com/Trivernis/whooshy}
|
* Abstract Route class to be implemented by each route.
|
||||||
*/
|
* This class contains the socket-io Server, router and resolver
|
||||||
|
* for each route.
|
||||||
import {Router} from "express";
|
*/
|
||||||
import {Namespace, Server} from "socket.io";
|
abstract class Route {
|
||||||
|
|
||||||
/**
|
public router?: Router;
|
||||||
* Abstract Route class to be implemented by each route.
|
protected io?: Server;
|
||||||
* This class contains the socket-io Server, router and resolver
|
protected ions?: Namespace;
|
||||||
* for each route.
|
|
||||||
*/
|
public abstract async init(...params: any): Promise<any>;
|
||||||
abstract class Route {
|
|
||||||
|
public abstract async destroy(...params: any): Promise<any>;
|
||||||
public router?: Router;
|
}
|
||||||
protected io?: Server;
|
|
||||||
protected ions?: Namespace;
|
export default Route;
|
||||||
|
|
||||||
public abstract async init(...params: any): Promise<any>;
|
|
||||||
public abstract async destroy(...params: any): Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Route;
|
|
||||||
|
@ -0,0 +1,317 @@
|
|||||||
|
import * as crypto from "crypto";
|
||||||
|
import * as sqz from "sequelize";
|
||||||
|
import {Sequelize} from "sequelize-typescript";
|
||||||
|
import {ChatNotFoundError} from "./errors/ChatNotFoundError";
|
||||||
|
import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError";
|
||||||
|
import {GroupNotFoundError} from "./errors/GroupNotFoundError";
|
||||||
|
import {InvalidLoginError} from "./errors/InvalidLoginError";
|
||||||
|
import {NoActionSpecifiedError} from "./errors/NoActionSpecifiedError";
|
||||||
|
import {UserNotFoundError} from "./errors/UserNotFoundError";
|
||||||
|
import globals from "./globals";
|
||||||
|
import {InternalEvents} from "./InternalEvents";
|
||||||
|
import * as models from "./models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new handle from the username and a base64 string of the current time.
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
async function generateHandle(username: string) {
|
||||||
|
username = username.toLowerCase().replace(/\s/g, "_");
|
||||||
|
const count = await models.User.count({where: {handle: {[sqz.Op.like]: `%${username}%`}}});
|
||||||
|
if (count > 0) {
|
||||||
|
return `${username}${count}`;
|
||||||
|
} else {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace with functions to fetch initial data for wrapping.
|
||||||
|
*/
|
||||||
|
namespace dataaccess {
|
||||||
|
|
||||||
|
let sequelize: Sequelize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes everything that needs to be initialized asynchronous.
|
||||||
|
*/
|
||||||
|
export async function init(seq: Sequelize) {
|
||||||
|
sequelize = seq;
|
||||||
|
try {
|
||||||
|
await sequelize.addModels([
|
||||||
|
models.ChatMember,
|
||||||
|
models.ChatMessage,
|
||||||
|
models.ChatRoom,
|
||||||
|
models.Friendship,
|
||||||
|
models.Post,
|
||||||
|
models.PostVote,
|
||||||
|
models.Request,
|
||||||
|
models.User,
|
||||||
|
models.Group,
|
||||||
|
models.GroupAdmin,
|
||||||
|
models.GroupMember,
|
||||||
|
models.EventParticipant,
|
||||||
|
models.Event,
|
||||||
|
]);
|
||||||
|
} catch (err) {
|
||||||
|
globals.logger.error(err.message);
|
||||||
|
globals.logger.debug(err.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the user by handle.
|
||||||
|
* @param userHandle
|
||||||
|
*/
|
||||||
|
export async function getUserByHandle(userHandle: string): Promise<models.User> {
|
||||||
|
const user = await models.User.findOne({where: {handle: userHandle}});
|
||||||
|
if (user) {
|
||||||
|
return user;
|
||||||
|
} else {
|
||||||
|
throw new UserNotFoundError(userHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the user by email and password
|
||||||
|
* @param email
|
||||||
|
* @param password
|
||||||
|
*/
|
||||||
|
export async function getUserByLogin(email: string, password: string): Promise<models.User> {
|
||||||
|
const hash = crypto.createHash("sha512");
|
||||||
|
hash.update(password);
|
||||||
|
password = hash.digest("hex");
|
||||||
|
const user = await models.User.findOne({where: {email}});
|
||||||
|
if (user) {
|
||||||
|
if (user.password === password) {
|
||||||
|
return user;
|
||||||
|
} else {
|
||||||
|
throw new InvalidLoginError(email);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new UserNotFoundError(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a user with a username and password returning a user
|
||||||
|
* @param username
|
||||||
|
* @param email
|
||||||
|
* @param password
|
||||||
|
*/
|
||||||
|
export async function registerUser(username: string, email: string, password: string): Promise<models.User> {
|
||||||
|
const hash = crypto.createHash("sha512");
|
||||||
|
hash.update(password);
|
||||||
|
password = hash.digest("hex");
|
||||||
|
const existResult = !!(await models.User.findOne({where: {username, email, password}}));
|
||||||
|
const handle = await generateHandle(username);
|
||||||
|
if (!existResult) {
|
||||||
|
return models.User.create({username, email, password, handle});
|
||||||
|
} else {
|
||||||
|
throw new EmailAlreadyRegisteredError(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a post for a given postId.s
|
||||||
|
* @param postId
|
||||||
|
*/
|
||||||
|
export async function getPost(postId: number): Promise<models.Post> {
|
||||||
|
const post = await models.Post.findByPk(postId);
|
||||||
|
if (post) {
|
||||||
|
return post;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all posts sorted by new or top with pagination.
|
||||||
|
* @param first
|
||||||
|
* @param offset
|
||||||
|
* @param sort
|
||||||
|
*/
|
||||||
|
export async function getPosts(first: number, offset: number, sort: SortType) {
|
||||||
|
if (sort === SortType.NEW) {
|
||||||
|
return models.Post.findAll({
|
||||||
|
include: [{association: "rVotes"}],
|
||||||
|
limit: first,
|
||||||
|
offset,
|
||||||
|
order: [["createdAt", "DESC"]],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return await sequelize.query(
|
||||||
|
`SELECT * FROM (
|
||||||
|
SELECT *,
|
||||||
|
(SELECT count(*) FROM post_votes WHERE vote_type = 'UPVOTE' AND post_id = posts.id) AS upvotes ,
|
||||||
|
(SELECT count(*) FROM post_votes WHERE vote_type = 'DOWNVOTE' AND post_id = posts.id) AS downvotes
|
||||||
|
FROM posts) AS a ORDER BY (a.upvotes - a.downvotes) DESC LIMIT ? OFFSET ?`,
|
||||||
|
{replacements: [first, offset], mapToModel: true, model: models.Post}) as models.Post[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a post
|
||||||
|
* @param content
|
||||||
|
* @param authorId
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
export async function createPost(content: string, authorId: number, type?: string): Promise<models.Post> {
|
||||||
|
type = type || "MISC";
|
||||||
|
const post = await models.Post.create({content, authorId});
|
||||||
|
globals.internalEmitter.emit(InternalEvents.POSTCREATE, post);
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a post
|
||||||
|
* @param postId
|
||||||
|
*/
|
||||||
|
export async function deletePost(postId: number): Promise<boolean> {
|
||||||
|
await (await models.Post.findByPk(postId)).destroy();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a chatroom containing two users
|
||||||
|
* @param members
|
||||||
|
*/
|
||||||
|
export async function createChat(...members: number[]): Promise<models.ChatRoom> {
|
||||||
|
return sequelize.transaction(async (t) => {
|
||||||
|
const chat = await models.ChatRoom.create({}, {transaction: t, include: [models.User]});
|
||||||
|
for (const member of members) {
|
||||||
|
const user = await models.User.findByPk(member);
|
||||||
|
await chat.$add("rMember", user, {transaction: t});
|
||||||
|
}
|
||||||
|
await chat.save({transaction: t});
|
||||||
|
globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat);
|
||||||
|
return chat;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message into a chat.
|
||||||
|
* @param authorId
|
||||||
|
* @param chatId
|
||||||
|
* @param content
|
||||||
|
*/
|
||||||
|
export async function sendChatMessage(authorId: number, chatId: number, content: string) {
|
||||||
|
const chat = await models.ChatRoom.findByPk(chatId);
|
||||||
|
if (chat) {
|
||||||
|
const message = await chat.$create("rMessage", {content, authorId}) as models.ChatMessage;
|
||||||
|
globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message);
|
||||||
|
return message;
|
||||||
|
} else {
|
||||||
|
throw new ChatNotFoundError(chatId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all rChats.
|
||||||
|
*/
|
||||||
|
export async function getAllChats(): Promise<models.ChatRoom[]> {
|
||||||
|
return models.ChatRoom.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a request to a user.
|
||||||
|
* @param sender
|
||||||
|
* @param receiver
|
||||||
|
* @param requestType
|
||||||
|
*/
|
||||||
|
export async function createRequest(sender: number, receiver: number, requestType?: RequestType) {
|
||||||
|
requestType = requestType || RequestType.FRIENDREQUEST;
|
||||||
|
|
||||||
|
const request = await models.Request.create({senderId: sender, receiverId: receiver, requestType});
|
||||||
|
globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new group.
|
||||||
|
* @param name
|
||||||
|
* @param creator
|
||||||
|
* @param members
|
||||||
|
*/
|
||||||
|
export async function createGroup(name: string, creator: number, members: number[]): Promise<models.Group> {
|
||||||
|
members = members || [];
|
||||||
|
return sequelize.transaction(async (t) => {
|
||||||
|
members.push(creator);
|
||||||
|
const groupChat = await createChat(...members);
|
||||||
|
const group = await models.Group.create({name, creatorId: creator, chatId: groupChat.id}, {transaction: t});
|
||||||
|
const creatorUser = await models.User.findByPk(creator, {transaction: t});
|
||||||
|
await group.$add("rAdmins", creatorUser, {transaction: t});
|
||||||
|
for (const member of members) {
|
||||||
|
const user = await models.User.findByPk(member, {transaction: t});
|
||||||
|
await group.$add("rMembers", user, {transaction: t});
|
||||||
|
}
|
||||||
|
return group;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the membership of a user
|
||||||
|
* @param groupId
|
||||||
|
* @param userId
|
||||||
|
* @param action
|
||||||
|
*/
|
||||||
|
export async function changeGroupMembership(groupId: number, userId: number, action: MembershipChangeAction):
|
||||||
|
Promise<models.Group> {
|
||||||
|
const group = await models.Group.findByPk(groupId);
|
||||||
|
if (group) {
|
||||||
|
const user = await models.User.findByPk(userId);
|
||||||
|
if (user) {
|
||||||
|
if (action === MembershipChangeAction.ADD) {
|
||||||
|
await group.$add("rMembers", user);
|
||||||
|
} else if (action === MembershipChangeAction.REMOVE) {
|
||||||
|
await group.$remove("rMembers", user);
|
||||||
|
} else if (action === MembershipChangeAction.OP) {
|
||||||
|
await group.$add("rAdmins", user);
|
||||||
|
} else if (action === MembershipChangeAction.DEOP) {
|
||||||
|
await group.$remove("rAdmins", user);
|
||||||
|
} else {
|
||||||
|
throw new NoActionSpecifiedError(MembershipChangeAction);
|
||||||
|
}
|
||||||
|
return group;
|
||||||
|
} else {
|
||||||
|
throw new UserNotFoundError(userId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new GroupNotFoundError(groupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum representing the types of votes that can be performed on a post.
|
||||||
|
*/
|
||||||
|
export enum VoteType {
|
||||||
|
UPVOTE = "UPVOTE",
|
||||||
|
DOWNVOTE = "DOWNVOTE",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum representing the types of request that can be created.
|
||||||
|
*/
|
||||||
|
export enum RequestType {
|
||||||
|
FRIENDREQUEST = "FRIENDREQUEST",
|
||||||
|
GROUPINVITE = "GROUPINVITE",
|
||||||
|
EVENTINVITE = "EVENTINVITE",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum representing the types of sorting in the feed.
|
||||||
|
*/
|
||||||
|
export enum SortType {
|
||||||
|
TOP = "TOP",
|
||||||
|
NEW = "NEW",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MembershipChangeAction {
|
||||||
|
ADD,
|
||||||
|
REMOVE,
|
||||||
|
OP,
|
||||||
|
DEOP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default dataaccess;
|
@ -1,31 +0,0 @@
|
|||||||
import markdown from "../markdown";
|
|
||||||
import {Chatroom} from "./Chatroom";
|
|
||||||
import {User} from "./User";
|
|
||||||
|
|
||||||
export class ChatMessage {
|
|
||||||
constructor(
|
|
||||||
public readonly author: User,
|
|
||||||
public readonly chat: Chatroom,
|
|
||||||
public readonly createdAt: number,
|
|
||||||
public readonly content: string) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The content rendered by markdown-it.
|
|
||||||
*/
|
|
||||||
public htmlContent(): string {
|
|
||||||
return markdown.renderInline(this.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns resolved and rendered content of the chat message.
|
|
||||||
*/
|
|
||||||
public resolvedContent() {
|
|
||||||
return {
|
|
||||||
author: this.author.id,
|
|
||||||
chat: this.chat.id,
|
|
||||||
content: this.content,
|
|
||||||
createdAt: this.createdAt,
|
|
||||||
htmlContent: this.htmlContent(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
import globals from "../globals";
|
|
||||||
import {ChatMessage} from "./ChatMessage";
|
|
||||||
import {queryHelper} from "./index";
|
|
||||||
import {User} from "./User";
|
|
||||||
|
|
||||||
export class Chatroom {
|
|
||||||
|
|
||||||
public namespace: string;
|
|
||||||
constructor(public readonly id: number) {
|
|
||||||
this.id = Number(id);
|
|
||||||
this.namespace = `/chat/${id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns if the chat exists.
|
|
||||||
*/
|
|
||||||
public async exists(): Promise<boolean> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "SELECT id FROM chats WHERE id = $1",
|
|
||||||
values: [this.id],
|
|
||||||
});
|
|
||||||
return !!result.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all members of a chatroom.
|
|
||||||
*/
|
|
||||||
public async members(): Promise<User[]> {
|
|
||||||
const result = await queryHelper.all({
|
|
||||||
cache: true,
|
|
||||||
text: `SELECT * FROM chat_members
|
|
||||||
JOIN users ON (chat_members.member = users.id)
|
|
||||||
WHERE chat_members.chat = $1;`,
|
|
||||||
values: [this.id],
|
|
||||||
});
|
|
||||||
const chatMembers = [];
|
|
||||||
for (const row of result) {
|
|
||||||
const user = new User(row.id, row);
|
|
||||||
chatMembers.push(user);
|
|
||||||
}
|
|
||||||
return chatMembers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns messages of the chat
|
|
||||||
* @param limit - the limit of messages to return
|
|
||||||
* @param offset - the offset of messages to return
|
|
||||||
* @param containing - filter by containing
|
|
||||||
*/
|
|
||||||
public async messages({first, offset, containing}: {first?: number, offset?: number, containing?: string}) {
|
|
||||||
const lim = first || 16;
|
|
||||||
const offs = offset || 0;
|
|
||||||
|
|
||||||
const result = await queryHelper.all({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT * FROM chat_messages WHERE chat = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
|
|
||||||
values: [this.id, lim, offs],
|
|
||||||
});
|
|
||||||
|
|
||||||
const messages = [];
|
|
||||||
const users: any = {};
|
|
||||||
for (const row of result) {
|
|
||||||
if (!users[row.author]) {
|
|
||||||
const user = new User(row.author);
|
|
||||||
await user.exists();
|
|
||||||
users[row.author] = user;
|
|
||||||
}
|
|
||||||
messages.push(new ChatMessage(users[row.author], this, row.created_at, row.content));
|
|
||||||
}
|
|
||||||
if (containing) {
|
|
||||||
return messages.filter((x) => x.content.includes(containing));
|
|
||||||
} else {
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
/**
|
|
||||||
* abstact DataObject class
|
|
||||||
*/
|
|
||||||
import {EventEmitter} from "events";
|
|
||||||
|
|
||||||
export abstract class DataObject extends EventEmitter {
|
|
||||||
protected dataLoaded: boolean = false;
|
|
||||||
private loadingData: boolean = false;
|
|
||||||
|
|
||||||
constructor(public id: number, protected row?: any) {
|
|
||||||
super();
|
|
||||||
this.id = Number(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns if the object extists by trying to load data.
|
|
||||||
*/
|
|
||||||
public async exists() {
|
|
||||||
await this.loadDataIfNotExists();
|
|
||||||
return this.dataLoaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract loadData(): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads data from the database if data has not been loaded
|
|
||||||
*/
|
|
||||||
protected async loadDataIfNotExists() {
|
|
||||||
if (!this.dataLoaded && !this.loadingData) {
|
|
||||||
this.loadingData = true;
|
|
||||||
await this.loadData();
|
|
||||||
this.loadingData = false;
|
|
||||||
this.emit("loaded");
|
|
||||||
} else if (this.loadingData) {
|
|
||||||
return new Promise((res) => {
|
|
||||||
this.on("loaded", () => res());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,159 +0,0 @@
|
|||||||
import markdown from "../markdown";
|
|
||||||
import {DataObject} from "./DataObject";
|
|
||||||
import {queryHelper} from "./index";
|
|
||||||
import dataaccess from "./index";
|
|
||||||
import {User} from "./User";
|
|
||||||
|
|
||||||
export class Post extends DataObject {
|
|
||||||
public readonly id: number;
|
|
||||||
private $createdAt: string;
|
|
||||||
private $content: string;
|
|
||||||
private $author: number;
|
|
||||||
private $type: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the resolved data of the post.
|
|
||||||
*/
|
|
||||||
public async resolvedData() {
|
|
||||||
await this.loadDataIfNotExists();
|
|
||||||
return {
|
|
||||||
authorId: this.$author,
|
|
||||||
content: this.$content,
|
|
||||||
createdAt: this.$createdAt,
|
|
||||||
id: this.id,
|
|
||||||
type: this.$type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Returns the upvotes of a post.
|
|
||||||
*/
|
|
||||||
public async upvotes(): Promise<number> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'UPVOTE'",
|
|
||||||
values: [this.id],
|
|
||||||
});
|
|
||||||
return result.count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the downvotes of the post
|
|
||||||
*/
|
|
||||||
public async downvotes(): Promise<number> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'DOWNVOTE'",
|
|
||||||
values: [this.id],
|
|
||||||
});
|
|
||||||
return result.count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The content of the post (markdown)
|
|
||||||
*/
|
|
||||||
public async content(): Promise<string> {
|
|
||||||
await this.loadDataIfNotExists();
|
|
||||||
return this.$content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the content rendered by markdown-it.
|
|
||||||
*/
|
|
||||||
public async htmlContent(): Promise<string> {
|
|
||||||
await this.loadDataIfNotExists();
|
|
||||||
return markdown.render(this.$content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The date the post was created at.
|
|
||||||
*/
|
|
||||||
public async createdAt(): Promise<string> {
|
|
||||||
await this.loadDataIfNotExists();
|
|
||||||
return this.$createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The autor of the post.
|
|
||||||
*/
|
|
||||||
public async author(): Promise<User> {
|
|
||||||
await this.loadDataIfNotExists();
|
|
||||||
return new User(this.$author);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the post.
|
|
||||||
*/
|
|
||||||
public async delete(): Promise<void> {
|
|
||||||
const query = await queryHelper.first({
|
|
||||||
text: "DELETE FROM posts WHERE id = $1",
|
|
||||||
values: [this.id],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of vote the user performed on the post.
|
|
||||||
*/
|
|
||||||
public async userVote(userId: number): Promise<dataaccess.VoteType> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT vote_type FROM votes WHERE user_id = $1 AND item_id = $2",
|
|
||||||
values: [userId, this.id],
|
|
||||||
});
|
|
||||||
if (result) {
|
|
||||||
return result.vote_type;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a vote on a post.
|
|
||||||
* @param userId
|
|
||||||
* @param type
|
|
||||||
*/
|
|
||||||
public async vote(userId: number, type: dataaccess.VoteType): Promise<dataaccess.VoteType> {
|
|
||||||
const uVote = await this.userVote(userId);
|
|
||||||
if (uVote === type) {
|
|
||||||
await queryHelper.first({
|
|
||||||
text: "DELETE FROM votes WHERE item_id = $1 AND user_id = $2",
|
|
||||||
values: [this.id, userId],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (uVote) {
|
|
||||||
await queryHelper.first({
|
|
||||||
text: "UPDATE votes SET vote_type = $1 WHERE user_id = $2 AND item_id = $3",
|
|
||||||
values: [type, userId, this.id],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await queryHelper.first({
|
|
||||||
text: "INSERT INTO votes (user_id, item_id, vote_type) values ($1, $2, $3)",
|
|
||||||
values: [userId, this.id, type],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the data from the database if needed.
|
|
||||||
*/
|
|
||||||
protected async loadData(): Promise<void> {
|
|
||||||
let result: any;
|
|
||||||
if (this.row) {
|
|
||||||
result = this.row;
|
|
||||||
} else {
|
|
||||||
result = await queryHelper.first({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT * FROM posts WHERE posts.id = $1",
|
|
||||||
values: [this.id],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (result) {
|
|
||||||
this.$author = result.author;
|
|
||||||
this.$content = result.content;
|
|
||||||
this.$createdAt = result.created_at;
|
|
||||||
this.$type = result.type;
|
|
||||||
this.dataLoaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,163 +0,0 @@
|
|||||||
import {RequestNotFoundError} from "../errors/RequestNotFoundError";
|
|
||||||
import {Chatroom} from "./Chatroom";
|
|
||||||
import dataaccess, {queryHelper} from "./index";
|
|
||||||
import {User} from "./User";
|
|
||||||
import {Request} from "./Request";
|
|
||||||
|
|
||||||
export class Profile extends User {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all chatrooms (with pagination).
|
|
||||||
* Skips the query if the user doesn't exist.
|
|
||||||
* @param first
|
|
||||||
* @param offset
|
|
||||||
*/
|
|
||||||
public async chats({first, offset}: {first: number, offset?: number}): Promise<Chatroom[]> {
|
|
||||||
if (!(await this.exists())) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
first = first || 10;
|
|
||||||
offset = offset || 0;
|
|
||||||
|
|
||||||
const result = await queryHelper.all({
|
|
||||||
text: "SELECT chat FROM chat_members WHERE member = $1 LIMIT $2 OFFSET $3",
|
|
||||||
values: [this.id, first, offset],
|
|
||||||
});
|
|
||||||
if (result) {
|
|
||||||
return result.map((row) => new Chatroom(row.chat));
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all open requests the user has send.
|
|
||||||
*/
|
|
||||||
public async sentRequests() {
|
|
||||||
const result = await queryHelper.all({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT * FROM requests WHERE sender = $1",
|
|
||||||
values: [this.id],
|
|
||||||
});
|
|
||||||
return this.getRequests(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all received requests of the user.
|
|
||||||
*/
|
|
||||||
public async receivedRequests() {
|
|
||||||
const result = await queryHelper.all({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT * FROM requests WHERE receiver = $1",
|
|
||||||
values: [this.id],
|
|
||||||
});
|
|
||||||
return this.getRequests(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the greenpoints of a user.
|
|
||||||
* @param points
|
|
||||||
*/
|
|
||||||
public async setGreenpoints(points: number): Promise<number> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "UPDATE users SET greenpoints = $1 WHERE id = $2 RETURNING greenpoints",
|
|
||||||
values: [points, this.id],
|
|
||||||
});
|
|
||||||
return result.greenpoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the email of the user
|
|
||||||
* @param email
|
|
||||||
*/
|
|
||||||
public async setEmail(email: string): Promise<string> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "UPDATE users SET email = $1 WHERE users.id = $2 RETURNING email",
|
|
||||||
values: [email, this.id],
|
|
||||||
});
|
|
||||||
return result.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the handle of the user
|
|
||||||
*/
|
|
||||||
public async setHandle(handle: string): Promise<string> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "UPDATE users SET handle = $1 WHERE id = $2",
|
|
||||||
values: [handle, this.id],
|
|
||||||
});
|
|
||||||
return result.handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the username of the user
|
|
||||||
* @param name
|
|
||||||
*/
|
|
||||||
public async setName(name: string): Promise<string> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "UPDATE users SET name = $1 WHERE id = $2",
|
|
||||||
values: [name, this.id],
|
|
||||||
});
|
|
||||||
return result.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Denys a request.
|
|
||||||
* @param sender
|
|
||||||
* @param type
|
|
||||||
*/
|
|
||||||
public async denyRequest(sender: number, type: dataaccess.RequestType) {
|
|
||||||
await queryHelper.first({
|
|
||||||
text: "DELETE FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3",
|
|
||||||
values: [this.id, sender, type],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Accepts a request.
|
|
||||||
* @param sender
|
|
||||||
* @param type
|
|
||||||
*/
|
|
||||||
public async acceptRequest(sender: number, type: dataaccess.RequestType) {
|
|
||||||
const exists = await queryHelper.first({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT 1 FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3",
|
|
||||||
values: [this.id, sender, type],
|
|
||||||
});
|
|
||||||
if (exists) {
|
|
||||||
if (type === dataaccess.RequestType.FRIENDREQUEST) {
|
|
||||||
await queryHelper.first({
|
|
||||||
text: "INSERT INTO user_friends (user_id, friend_id) VALUES ($1, $2)",
|
|
||||||
values: [this.id, sender],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new RequestNotFoundError(sender, this.id, type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns request wrapper for a row database request result.
|
|
||||||
* @param rows
|
|
||||||
*/
|
|
||||||
private getRequests(rows: any) {
|
|
||||||
const requests = [];
|
|
||||||
const requestUsers: any = {};
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
let sender = requestUsers[row.sender];
|
|
||||||
|
|
||||||
if (!sender) {
|
|
||||||
sender = new User(row.sender);
|
|
||||||
requestUsers[row.sender] = sender;
|
|
||||||
}
|
|
||||||
let receiver = requestUsers[row.receiver];
|
|
||||||
if (!receiver) {
|
|
||||||
receiver = new User(row.receiver);
|
|
||||||
requestUsers[row.receiver] = receiver;
|
|
||||||
}
|
|
||||||
requests.push(new Request(sender, receiver, row.type));
|
|
||||||
}
|
|
||||||
return requests;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import dataaccess from "./index";
|
|
||||||
import {User} from "./User";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a request to a user.
|
|
||||||
*/
|
|
||||||
export class Request {
|
|
||||||
constructor(
|
|
||||||
public readonly sender: User,
|
|
||||||
public readonly receiver: User,
|
|
||||||
public readonly type: dataaccess.RequestType) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the resolved request data.
|
|
||||||
*/
|
|
||||||
public resolvedData() {
|
|
||||||
return {
|
|
||||||
receiverId: this.receiver.id,
|
|
||||||
senderId: this.sender.id,
|
|
||||||
type: this.type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,128 +0,0 @@
|
|||||||
import globals from "../globals";
|
|
||||||
import {DataObject} from "./DataObject";
|
|
||||||
import {queryHelper} from "./index";
|
|
||||||
import {Post} from "./Post";
|
|
||||||
|
|
||||||
export class User extends DataObject {
|
|
||||||
private $name: string;
|
|
||||||
private $handle: string;
|
|
||||||
private $email: string;
|
|
||||||
private $greenpoints: number;
|
|
||||||
private $joinedAt: string;
|
|
||||||
private $exists: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the user
|
|
||||||
*/
|
|
||||||
public async name(): Promise<string> {
|
|
||||||
await this.loadDataIfNotExists();
|
|
||||||
return this.$name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The unique handle of the user.
|
|
||||||
*/
|
|
||||||
public async handle(): Promise<string> {
|
|
||||||
await this.loadDataIfNotExists();
|
|
||||||
return this.$handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The email of the user
|
|
||||||
*/
|
|
||||||
public async email(): Promise<string> {
|
|
||||||
await this.loadDataIfNotExists();
|
|
||||||
return this.$email;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of greenpoints of the user
|
|
||||||
*/
|
|
||||||
public async greenpoints(): Promise<number> {
|
|
||||||
await this.loadDataIfNotExists();
|
|
||||||
return this.$greenpoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of posts the user created
|
|
||||||
*/
|
|
||||||
public async numberOfPosts(): Promise<number> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT COUNT(*) count FROM posts WHERE author = $1",
|
|
||||||
values: [this.id],
|
|
||||||
});
|
|
||||||
return result.count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The date the user joined the platform
|
|
||||||
*/
|
|
||||||
public async joinedAt(): Promise<Date> {
|
|
||||||
await this.loadDataIfNotExists();
|
|
||||||
return new Date(this.$joinedAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all friends of the user.
|
|
||||||
*/
|
|
||||||
public async friends(): Promise<User[]> {
|
|
||||||
const result = await queryHelper.all({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT * FROM user_friends WHERE user_id = $1 OR friend_id = $1",
|
|
||||||
values: [this.id],
|
|
||||||
});
|
|
||||||
const userFriends = [];
|
|
||||||
for (const row of result) {
|
|
||||||
if (row.user_id === this.id) {
|
|
||||||
userFriends.push(new User(row.friend_id));
|
|
||||||
} else {
|
|
||||||
userFriends.push(new User(row.user_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return userFriends;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all posts for a user.
|
|
||||||
*/
|
|
||||||
public async posts({first, offset}: {first: number, offset: number}): Promise<Post[]> {
|
|
||||||
first = first || 10;
|
|
||||||
offset = offset || 0;
|
|
||||||
const result = await queryHelper.all({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT * FROM posts WHERE author = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
|
|
||||||
values: [this.id, first, offset],
|
|
||||||
});
|
|
||||||
const posts = [];
|
|
||||||
|
|
||||||
for (const row of result) {
|
|
||||||
posts.push(new Post(row.id, row));
|
|
||||||
}
|
|
||||||
return posts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the data for the user.
|
|
||||||
*/
|
|
||||||
protected async loadData(): Promise<void> {
|
|
||||||
let result: any;
|
|
||||||
if (this.row) {
|
|
||||||
result = this.row;
|
|
||||||
} else {
|
|
||||||
result = await queryHelper.first({
|
|
||||||
cache: true,
|
|
||||||
text: "SELECT * FROM users WHERE users.id = $1",
|
|
||||||
values: [this.id],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (result) {
|
|
||||||
this.$name = result.name;
|
|
||||||
this.$handle = result.handle;
|
|
||||||
this.$email = result.email;
|
|
||||||
this.$greenpoints = result.greenpoints;
|
|
||||||
this.$joinedAt = result.joined_at;
|
|
||||||
this.dataLoaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,258 +0,0 @@
|
|||||||
import {Pool} from "pg";
|
|
||||||
import {ChatNotFoundError} from "../errors/ChatNotFoundError";
|
|
||||||
import {EmailAlreadyRegisteredError} from "../errors/EmailAlreadyRegisteredError";
|
|
||||||
import {UserNotFoundError} from "../errors/UserNotFoundError";
|
|
||||||
import globals from "../globals";
|
|
||||||
import {InternalEvents} from "../InternalEvents";
|
|
||||||
import {QueryHelper} from "../QueryHelper";
|
|
||||||
import {ChatMessage} from "./ChatMessage";
|
|
||||||
import {Chatroom} from "./Chatroom";
|
|
||||||
import {Post} from "./Post";
|
|
||||||
import {Profile} from "./Profile";
|
|
||||||
import {Request} from "./Request";
|
|
||||||
import {User} from "./User";
|
|
||||||
|
|
||||||
const config = globals.config;
|
|
||||||
const tableCreationFile = __dirname + "/../../sql/create-tables.sql";
|
|
||||||
const tableUpdateFile = __dirname + "/../../sql/update-tables.sql";
|
|
||||||
|
|
||||||
const dbClient: Pool = new Pool({
|
|
||||||
database: config.database.database,
|
|
||||||
host: config.database.host,
|
|
||||||
password: config.database.password,
|
|
||||||
port: config.database.port,
|
|
||||||
user: config.database.user,
|
|
||||||
});
|
|
||||||
export const queryHelper = new QueryHelper(dbClient, tableCreationFile, tableUpdateFile);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a new handle from the username and a base64 string of the current time.
|
|
||||||
* @param username
|
|
||||||
*/
|
|
||||||
function generateHandle(username: string) {
|
|
||||||
return `${username}.${Buffer.from(Date.now().toString()).toString("base64")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Namespace with functions to fetch initial data for wrapping.
|
|
||||||
*/
|
|
||||||
namespace dataaccess {
|
|
||||||
|
|
||||||
export const pool: Pool = dbClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes everything that needs to be initialized asynchronous.
|
|
||||||
*/
|
|
||||||
export async function init() {
|
|
||||||
try {
|
|
||||||
await queryHelper.init();
|
|
||||||
} catch (err) {
|
|
||||||
globals.logger.error(err.message);
|
|
||||||
globals.logger.debug(err.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the user by handle.
|
|
||||||
* @param userHandle
|
|
||||||
*/
|
|
||||||
export async function getUserByHandle(userHandle: string): Promise<User> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "SELECT * FROM users WHERE users.handle = $1",
|
|
||||||
values: [userHandle],
|
|
||||||
});
|
|
||||||
if (result) {
|
|
||||||
return new User(result.id, result);
|
|
||||||
} else {
|
|
||||||
throw new UserNotFoundError(userHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the user by email and password
|
|
||||||
* @param email
|
|
||||||
* @param password
|
|
||||||
*/
|
|
||||||
export async function getUserByLogin(email: string, password: string): Promise<Profile> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "SELECT * FROM users WHERE email = $1 AND password = $2",
|
|
||||||
values: [email, password],
|
|
||||||
});
|
|
||||||
if (result) {
|
|
||||||
return new Profile(result.id, result);
|
|
||||||
} else {
|
|
||||||
throw new UserNotFoundError(email);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a user with a username and password returning a user
|
|
||||||
* @param username
|
|
||||||
* @param email
|
|
||||||
* @param password
|
|
||||||
*/
|
|
||||||
export async function registerUser(username: string, email: string, password: string) {
|
|
||||||
const existResult = await queryHelper.first({
|
|
||||||
text: "SELECT email FROM users WHERE email = $1;",
|
|
||||||
values: [email],
|
|
||||||
});
|
|
||||||
if (!existResult || !existResult.email) {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "INSERT INTO users (name, handle, password, email) VALUES ($1, $2, $3, $4) RETURNING *",
|
|
||||||
values: [username, generateHandle(username), password, email],
|
|
||||||
});
|
|
||||||
return new Profile(result.id, result);
|
|
||||||
} else {
|
|
||||||
throw new EmailAlreadyRegisteredError(email);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a post for a given postId.s
|
|
||||||
* @param postId
|
|
||||||
*/
|
|
||||||
export async function getPost(postId: number): Promise<Post> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "SELECT * FROM posts WHERE id = $1",
|
|
||||||
values: [postId],
|
|
||||||
});
|
|
||||||
if (result) {
|
|
||||||
return new Post(result.id, result);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a post
|
|
||||||
* @param content
|
|
||||||
* @param authorId
|
|
||||||
* @param type
|
|
||||||
*/
|
|
||||||
export async function createPost(content: string, authorId: number, type?: string): Promise<Post> {
|
|
||||||
type = type || "MISC";
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *",
|
|
||||||
values: [content, authorId, type],
|
|
||||||
});
|
|
||||||
const post = new Post(result.id, result);
|
|
||||||
globals.internalEmitter.emit(InternalEvents.POSTCREATE, post);
|
|
||||||
return post;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a post
|
|
||||||
* @param postId
|
|
||||||
*/
|
|
||||||
export async function deletePost(postId: number): Promise<boolean> {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "DELETE FROM posts WHERE posts.id = $1",
|
|
||||||
values: [postId],
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a chatroom containing two users
|
|
||||||
* @param members
|
|
||||||
*/
|
|
||||||
export async function createChat(...members: number[]): Promise<Chatroom> {
|
|
||||||
const idResult = await queryHelper.first({
|
|
||||||
text: "INSERT INTO chats (id) values (default) RETURNING *;",
|
|
||||||
});
|
|
||||||
const id = idResult.id;
|
|
||||||
const transaction = await queryHelper.createTransaction();
|
|
||||||
try {
|
|
||||||
await transaction.begin();
|
|
||||||
for (const member of members) {
|
|
||||||
await transaction.query({
|
|
||||||
name: "chat-member-insert",
|
|
||||||
text: "INSERT INTO chat_members (chat, member) VALUES ($1, $2);",
|
|
||||||
values: [id, member],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (err) {
|
|
||||||
globals.logger.warn(`Failed to insert chatmember into database: ${err.message}`);
|
|
||||||
globals.logger.debug(err.stack);
|
|
||||||
await transaction.rollback();
|
|
||||||
} finally {
|
|
||||||
transaction.release();
|
|
||||||
}
|
|
||||||
const chat = new Chatroom(id);
|
|
||||||
globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat);
|
|
||||||
return chat;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a message into a chat.
|
|
||||||
* @param authorId
|
|
||||||
* @param chatId
|
|
||||||
* @param content
|
|
||||||
*/
|
|
||||||
export async function sendChatMessage(authorId: number, chatId: number, content: string) {
|
|
||||||
const chat = new Chatroom(chatId);
|
|
||||||
if ((await chat.exists())) {
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "INSERT INTO chat_messages (chat, author, content) values ($1, $2, $3) RETURNING *",
|
|
||||||
values: [chatId, authorId, content],
|
|
||||||
});
|
|
||||||
const message = new ChatMessage(new User(result.author), chat, result.created_at, result.content);
|
|
||||||
globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message);
|
|
||||||
return message;
|
|
||||||
} else {
|
|
||||||
throw new ChatNotFoundError(chatId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all chats.
|
|
||||||
*/
|
|
||||||
export async function getAllChats(): Promise<Chatroom[]> {
|
|
||||||
const result = await queryHelper.all({
|
|
||||||
text: "SELECT id FROM chats;",
|
|
||||||
});
|
|
||||||
const chats = [];
|
|
||||||
for (const row of result) {
|
|
||||||
chats.push(new Chatroom(row.id));
|
|
||||||
}
|
|
||||||
return chats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a request to a user.
|
|
||||||
* @param sender
|
|
||||||
* @param receiver
|
|
||||||
* @param type
|
|
||||||
*/
|
|
||||||
export async function createRequest(sender: number, receiver: number, type?: RequestType) {
|
|
||||||
type = type || RequestType.FRIENDREQUEST;
|
|
||||||
|
|
||||||
const result = await queryHelper.first({
|
|
||||||
text: "INSERT INTO requests (sender, receiver, type) VALUES ($1, $2, $3) RETURNING *",
|
|
||||||
values: [sender, receiver, type],
|
|
||||||
});
|
|
||||||
const request = new Request(new User(result.sender), new User(result.receiver), result.type);
|
|
||||||
globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, Request);
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enum representing the types of votes that can be performed on a post.
|
|
||||||
*/
|
|
||||||
export enum VoteType {
|
|
||||||
UPVOTE = "UPVOTE",
|
|
||||||
DOWNVOTE = "DOWNVOTE",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enum representing the types of request that can be created.
|
|
||||||
*/
|
|
||||||
export enum RequestType {
|
|
||||||
FRIENDREQUEST = "FRIENDREQUEST",
|
|
||||||
GROUPINVITE = "GROUPINVITE",
|
|
||||||
EVENTINVITE = "EVENTINVITE",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default dataaccess;
|
|
@ -1,13 +1,13 @@
|
|||||||
import {GraphQLError} from "graphql";
|
import {GraphQLError} from "graphql";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base error class.
|
* Base error class.
|
||||||
*/
|
*/
|
||||||
export class BaseError extends Error {
|
export class BaseError extends Error {
|
||||||
public readonly graphqlError: GraphQLError;
|
public readonly graphqlError: GraphQLError;
|
||||||
|
|
||||||
constructor(message?: string, friendlyMessage?: string) {
|
constructor(message?: string, friendlyMessage?: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.graphqlError = new GraphQLError(friendlyMessage || message);
|
this.graphqlError = new GraphQLError(friendlyMessage || message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {BaseError} from "./BaseError";
|
import {BaseError} from "./BaseError";
|
||||||
|
|
||||||
export class ChatNotFoundError extends BaseError {
|
export class ChatNotFoundError extends BaseError {
|
||||||
constructor(chatId: number) {
|
constructor(chatId: number) {
|
||||||
super(`Chat with id ${chatId} not found.`);
|
super(`Chat with id ${chatId} not found.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {BaseError} from "./BaseError";
|
import {BaseError} from "./BaseError";
|
||||||
|
|
||||||
export class EmailAlreadyRegisteredError extends BaseError {
|
export class EmailAlreadyRegisteredError extends BaseError {
|
||||||
constructor(email: string) {
|
constructor(email: string) {
|
||||||
super(`A user for '${email}' does already exist.`);
|
super(`A user for '${email}' does already exist.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import {BaseError} from "./BaseError";
|
||||||
|
|
||||||
|
export class GroupNotFoundError extends BaseError {
|
||||||
|
constructor(groupId: number) {
|
||||||
|
super(`Group ${groupId} not found!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import {BaseError} from "./BaseError";
|
||||||
|
|
||||||
|
export class InvalidLoginError extends BaseError {
|
||||||
|
constructor(email: (string)) {
|
||||||
|
super(`Invalid login data for ${email}.`);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import {BaseError} from "./BaseError";
|
||||||
|
|
||||||
|
export class NoActionSpecifiedError extends BaseError {
|
||||||
|
constructor(actions?: any) {
|
||||||
|
if (actions) {
|
||||||
|
super(`No action of '${Object.keys(actions).join(", ")}'`);
|
||||||
|
} else {
|
||||||
|
super("No action specified!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import dataaccess from "../dataaccess";
|
import dataaccess from "../dataaccess";
|
||||||
import {BaseError} from "./BaseError";
|
import {BaseError} from "./BaseError";
|
||||||
|
|
||||||
export class RequestNotFoundError extends BaseError {
|
export class RequestNotFoundError extends BaseError {
|
||||||
constructor(sender: number, receiver: number, type: dataaccess.RequestType) {
|
constructor(sender: number, receiver: number, type: dataaccess.RequestType) {
|
||||||
super(`Request with sender '${sender}' and receiver '${receiver}' of type '${type}' not found.`);
|
super(`Request with sender '${sender}' and receiver '${receiver}' of type '${type}' not found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {BaseError} from "./BaseError";
|
import {BaseError} from "./BaseError";
|
||||||
|
|
||||||
export class UserNotFoundError extends BaseError {
|
export class UserNotFoundError extends BaseError {
|
||||||
constructor(username: string) {
|
constructor(username: (string|number)) {
|
||||||
super(`User ${username} not found!`);
|
super(`User ${username} not found!`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
import {GraphQLError} from "graphql";
|
import {GraphQLError} from "graphql";
|
||||||
import {BaseError} from "./BaseError";
|
|
||||||
|
export class NotLoggedInGqlError extends GraphQLError {
|
||||||
export class NotLoggedInGqlError extends GraphQLError {
|
constructor() {
|
||||||
|
super("Not logged in");
|
||||||
constructor() {
|
}
|
||||||
super("Not logged in");
|
}
|
||||||
}
|
|
||||||
}
|
export class PostNotFoundGqlError extends GraphQLError {
|
||||||
|
constructor(postId: number) {
|
||||||
|
super(`Post '${postId}' not found!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GroupNotFoundGqlError extends GraphQLError {
|
||||||
|
constructor(groupId: number) {
|
||||||
|
super(`Group '${groupId}' not found!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,36 +1,36 @@
|
|||||||
import * as MarkdownIt from "markdown-it/lib";
|
import * as MarkdownIt from "markdown-it/lib";
|
||||||
import globals from "./globals";
|
import globals from "./globals";
|
||||||
|
|
||||||
namespace markdown {
|
namespace markdown {
|
||||||
|
|
||||||
const md = new MarkdownIt();
|
const md = new MarkdownIt();
|
||||||
|
|
||||||
for (const pluginName of globals.config.markdown.plugins) {
|
for (const pluginName of globals.config.markdown.plugins) {
|
||||||
try {
|
try {
|
||||||
const plugin = require(pluginName);
|
const plugin = require(pluginName);
|
||||||
if (plugin) {
|
if (plugin) {
|
||||||
md.use(plugin);
|
md.use(plugin);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
globals.logger.warn(`Markdown-it plugin '${pluginName}' not found!`);
|
globals.logger.warn(`Markdown-it plugin '${pluginName}' not found!`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the markdown string inline (without blocks).
|
* Renders the markdown string inline (without blocks).
|
||||||
* @param markdownString
|
* @param markdownString
|
||||||
*/
|
*/
|
||||||
export function renderInline(markdownString: string) {
|
export function renderInline(markdownString: string) {
|
||||||
return md.renderInline(markdownString);
|
return md.renderInline(markdownString);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the markdown string.
|
* Renders the markdown string.
|
||||||
* @param markdownString
|
* @param markdownString
|
||||||
*/
|
*/
|
||||||
export function render(markdownString: string) {
|
export function render(markdownString: string) {
|
||||||
return md.render(markdownString);
|
return md.render(markdownString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default markdown;
|
export default markdown;
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||||
|
import {ChatRoom} from "./ChatRoom";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class ChatMember extends Model<ChatMember> {
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public userId: number;
|
||||||
|
|
||||||
|
@ForeignKey(() => ChatRoom)
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public chatId: number;
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
import * as sqz from "sequelize";
|
||||||
|
import {BelongsTo, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||||
|
import markdown from "../markdown";
|
||||||
|
import {ChatRoom} from "./ChatRoom";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class ChatMessage extends Model<ChatMessage> {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Column({type: sqz.STRING(512), allowNull: false})
|
||||||
|
public content: string;
|
||||||
|
|
||||||
|
@ForeignKey(() => ChatRoom)
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public chatId: number;
|
||||||
|
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public authorId: number;
|
||||||
|
|
||||||
|
@BelongsTo(() => ChatRoom, "chatId")
|
||||||
|
public rChat: ChatRoom;
|
||||||
|
|
||||||
|
@BelongsTo(() => User, "authorId")
|
||||||
|
public rAuthor: User;
|
||||||
|
|
||||||
|
@CreatedAt
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
public async chat(): Promise<ChatRoom> {
|
||||||
|
return await this.$get("rChat") as ChatRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async author(): Promise<User> {
|
||||||
|
return await this.$get("rAuthor") as User;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get htmlContent(): string {
|
||||||
|
return markdown.renderInline(this.getDataValue("content"));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import {BelongsToMany, CreatedAt, HasMany, Model, Table,} from "sequelize-typescript";
|
||||||
|
import {ChatMember} from "./ChatMember";
|
||||||
|
import {ChatMessage} from "./ChatMessage";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class ChatRoom extends Model<ChatRoom> {
|
||||||
|
@BelongsToMany(() => User, () => ChatMember)
|
||||||
|
public rMembers: User[];
|
||||||
|
|
||||||
|
@HasMany(() => ChatMessage, "chatId")
|
||||||
|
public rMessages: ChatMessage[];
|
||||||
|
|
||||||
|
@CreatedAt
|
||||||
|
public readonly createdAt!: Date;
|
||||||
|
|
||||||
|
public async members(): Promise<User[]> {
|
||||||
|
return await this.$get("rMembers") as User[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async messages(): Promise<ChatMessage[]> {
|
||||||
|
return await this.$get("rMessages") as ChatMessage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get namespace(): string {
|
||||||
|
return "/chats/" + this.getDataValue("id");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import {BelongsTo, BelongsToMany, Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
|
||||||
|
import {EventParticipant} from "./EventParticipant";
|
||||||
|
import {Group} from "./Group";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class Event extends Model<Event> {
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public name: string;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public dueDate: Date;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ForeignKey(() => Group)
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public groupId: number;
|
||||||
|
|
||||||
|
@BelongsTo(() => Group, "groupId")
|
||||||
|
public rGroup: Group;
|
||||||
|
|
||||||
|
@BelongsToMany(() => User, () => EventParticipant)
|
||||||
|
public rParticipants: User[];
|
||||||
|
|
||||||
|
public async group(): Promise<Group> {
|
||||||
|
return await this.$get("rGroup") as Group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async participants({first, offset}: {first: number, offset: number}): Promise<User[]> {
|
||||||
|
const limit = first || 10;
|
||||||
|
offset = offset || 0;
|
||||||
|
return await this.$get("rParticipants", {limit, offset}) as User[];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import {BelongsTo, BelongsToMany, Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
|
||||||
|
import {Event} from "./Event";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class EventParticipant extends Model<EventParticipant> {
|
||||||
|
@NotNull
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public userId: number;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ForeignKey(() => Event)
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public eventId: number;
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import {Column, ForeignKey, Model, NotNull, PrimaryKey, Table} from "sequelize-typescript";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class Friendship extends Model<Friendship> {
|
||||||
|
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@PrimaryKey
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public userId: number;
|
||||||
|
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@PrimaryKey
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public friendId: number;
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
import {BelongsTo, BelongsToMany, Column, ForeignKey, HasMany, Model, NotNull, Table} from "sequelize-typescript";
|
||||||
|
import {ChatRoom} from "./ChatRoom";
|
||||||
|
import {Event} from "./Event";
|
||||||
|
import {GroupAdmin} from "./GroupAdmin";
|
||||||
|
import {GroupMember} from "./GroupMember";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class Group extends Model<Group> {
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public name: string;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public creatorId: number;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ForeignKey(() => ChatRoom)
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public chatId: number;
|
||||||
|
|
||||||
|
@BelongsTo(() => User, "creatorId")
|
||||||
|
public rCreator: User;
|
||||||
|
|
||||||
|
@BelongsToMany(() => User, () => GroupAdmin)
|
||||||
|
public rAdmins: User[];
|
||||||
|
|
||||||
|
@BelongsToMany(() => User, () => GroupMember)
|
||||||
|
public rMembers: User[];
|
||||||
|
|
||||||
|
@BelongsTo(() => ChatRoom)
|
||||||
|
public rChat: ChatRoom;
|
||||||
|
|
||||||
|
@HasMany(() => Event, "groupId")
|
||||||
|
public rEvents: Event[];
|
||||||
|
|
||||||
|
public async creator(): Promise<User> {
|
||||||
|
return await this.$get("rCreator") as User;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async admins({first, offset}: { first: number, offset: number }): Promise<User[]> {
|
||||||
|
const limit = first || 10;
|
||||||
|
offset = offset || 0;
|
||||||
|
return await this.$get("rAdmins", {limit, offset}) as User[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async members({first, offset}: { first: number, offset: number }): Promise<User[]> {
|
||||||
|
const limit = first || 10;
|
||||||
|
offset = offset || 0;
|
||||||
|
return await this.$get("rMembers", {limit, offset}) as User[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async chat(): Promise<ChatRoom> {
|
||||||
|
return await this.$get("rChat") as ChatRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async events({first, offset}: { first: number, offset: number }): Promise<Event[]> {
|
||||||
|
const limit = first || 10;
|
||||||
|
offset = offset || 0;
|
||||||
|
return await this.$get("rEvents", {limit, offset}) as Event[];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||||
|
import {Group} from "./Group";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class GroupAdmin extends Model<GroupAdmin> {
|
||||||
|
@NotNull
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public userId: number;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ForeignKey(() => Group)
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public groupId: number;
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||||
|
import {Group} from "./Group";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class GroupMember extends Model<GroupMember> {
|
||||||
|
@NotNull
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public userId: number;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ForeignKey(() => Group)
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public groupId: number;
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
import * as sqz from "sequelize";
|
||||||
|
import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||||
|
import markdown from "../markdown";
|
||||||
|
import {PostVote, VoteType} from "./PostVote";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class Post extends Model<Post> {
|
||||||
|
@NotNull
|
||||||
|
@Column({type: sqz.STRING(2048), allowNull: false})
|
||||||
|
public content: string;
|
||||||
|
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public authorId: number;
|
||||||
|
|
||||||
|
@BelongsTo(() => User, "authorId")
|
||||||
|
public rAuthor: User;
|
||||||
|
|
||||||
|
@BelongsToMany(() => User, () => PostVote)
|
||||||
|
public rVotes: Array<User & {PostVote: PostVote}>;
|
||||||
|
|
||||||
|
@CreatedAt
|
||||||
|
public readonly createdAt!: Date;
|
||||||
|
|
||||||
|
public async author(): Promise<User> {
|
||||||
|
return await this.$get("rAuthor") as User;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async votes(): Promise<Array<User & {PostVote: PostVote}>> {
|
||||||
|
return await this.$get("rVotes") as Array<User & {PostVote: PostVote}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get htmlContent() {
|
||||||
|
return markdown.render(this.getDataValue("content"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async upvotes() {
|
||||||
|
return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.UPVOTE).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downvotes() {
|
||||||
|
return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.DOWNVOTE).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async vote(userId: number, type: VoteType): Promise<VoteType> {
|
||||||
|
type = type || VoteType.UPVOTE;
|
||||||
|
let votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>;
|
||||||
|
let vote = votes[0] || null;
|
||||||
|
let created = false;
|
||||||
|
if (!vote) {
|
||||||
|
await this.$add("rVote", userId);
|
||||||
|
votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>;
|
||||||
|
vote = votes[0] || null;
|
||||||
|
created = true;
|
||||||
|
}
|
||||||
|
if (vote) {
|
||||||
|
if (vote.PostVote.voteType === type && !created) {
|
||||||
|
await vote.PostVote.destroy();
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
vote.PostVote.voteType = type;
|
||||||
|
await vote.PostVote.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vote.PostVote.voteType;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import * as sqz from "sequelize";
|
||||||
|
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||||
|
import {Post} from "./Post";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
export enum VoteType {
|
||||||
|
UPVOTE = "UPVOTE",
|
||||||
|
DOWNVOTE = "DOWNVOTE",
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class PostVote extends Model<PostVote> {
|
||||||
|
@NotNull
|
||||||
|
@Column({type: sqz.ENUM, values: ["UPVOTE", "DOWNVOTE"], defaultValue: "UPVOTE", allowNull: false})
|
||||||
|
public voteType: VoteType;
|
||||||
|
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public userId: number;
|
||||||
|
|
||||||
|
@ForeignKey(() => Post)
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public postId: number;
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import * as sqz from "sequelize";
|
||||||
|
import {BelongsTo, Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
|
||||||
|
import {User} from "./User";
|
||||||
|
|
||||||
|
export enum RequestType {
|
||||||
|
FRIENDREQUEST = "FRIENDREQUEST",
|
||||||
|
GROUPINVITE = "GROUPINVITE",
|
||||||
|
EVENTINVITE = "EVENTINVITE",
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class Request extends Model<Request> {
|
||||||
|
@NotNull
|
||||||
|
@Column({
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: "FRIENDREQUEST",
|
||||||
|
type: sqz.ENUM,
|
||||||
|
values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"],
|
||||||
|
})
|
||||||
|
public requestType: RequestType;
|
||||||
|
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public senderId: number;
|
||||||
|
|
||||||
|
@BelongsTo(() => User, "senderId")
|
||||||
|
public rSender: User;
|
||||||
|
|
||||||
|
@ForeignKey(() => User)
|
||||||
|
@NotNull
|
||||||
|
@Column({allowNull: false})
|
||||||
|
public receiverId: number;
|
||||||
|
|
||||||
|
@BelongsTo(() => User, "receiverId")
|
||||||
|
public rReceiver: User;
|
||||||
|
|
||||||
|
public async receiver(): Promise<User> {
|
||||||
|
return await this.$get("rReceiver") as User;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sender(): Promise<User> {
|
||||||
|
return await this.$get("rSender") as User;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,288 @@
|
|||||||
|
import * as sqz from "sequelize";
|
||||||
|
import {
|
||||||
|
BelongsToMany,
|
||||||
|
Column,
|
||||||
|
CreatedAt,
|
||||||
|
HasMany,
|
||||||
|
Model,
|
||||||
|
NotNull,
|
||||||
|
Table,
|
||||||
|
Unique,
|
||||||
|
UpdatedAt,
|
||||||
|
} from "sequelize-typescript";
|
||||||
|
import {RequestNotFoundError} from "../errors/RequestNotFoundError";
|
||||||
|
import {UserNotFoundError} from "../errors/UserNotFoundError";
|
||||||
|
import {ChatMember} from "./ChatMember";
|
||||||
|
import {ChatMessage} from "./ChatMessage";
|
||||||
|
import {ChatRoom} from "./ChatRoom";
|
||||||
|
import {Event} from "./Event";
|
||||||
|
import {EventParticipant} from "./EventParticipant";
|
||||||
|
import {Friendship} from "./Friendship";
|
||||||
|
import {Group} from "./Group";
|
||||||
|
import {GroupAdmin} from "./GroupAdmin";
|
||||||
|
import {GroupMember} from "./GroupMember";
|
||||||
|
import {Post} from "./Post";
|
||||||
|
import {PostVote} from "./PostVote";
|
||||||
|
import {Request, RequestType} from "./Request";
|
||||||
|
|
||||||
|
@Table({underscored: true})
|
||||||
|
export class User extends Model<User> {
|
||||||
|
@NotNull
|
||||||
|
@Column({type: sqz.STRING(128), allowNull: false})
|
||||||
|
public username: string;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Unique
|
||||||
|
@Column({type: sqz.STRING(128), allowNull: false, unique: true})
|
||||||
|
public handle: string;
|
||||||
|
|
||||||
|
@Unique
|
||||||
|
@NotNull
|
||||||
|
@Column({type: sqz.STRING(128), allowNull: false, unique: true})
|
||||||
|
public email: string;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Column({type: sqz.STRING(128), allowNull: false})
|
||||||
|
public password: string;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Column({defaultValue: 0, allowNull: false})
|
||||||
|
public rankpoints: number;
|
||||||
|
|
||||||
|
@BelongsToMany(() => User, () => Friendship, "userId")
|
||||||
|
public rFriends: User[];
|
||||||
|
|
||||||
|
@BelongsToMany(() => User, () => Friendship, "friendId")
|
||||||
|
public rFriendOf: User[];
|
||||||
|
|
||||||
|
@BelongsToMany(() => Post, () => PostVote)
|
||||||
|
public votes: Array<Post & { PostVote: PostVote }>;
|
||||||
|
|
||||||
|
@BelongsToMany(() => ChatRoom, () => ChatMember)
|
||||||
|
public rChats: ChatRoom[];
|
||||||
|
|
||||||
|
@BelongsToMany(() => Group, () => GroupAdmin)
|
||||||
|
public rAdministratedGroups: Group[];
|
||||||
|
|
||||||
|
@BelongsToMany(() => Event, () => EventParticipant)
|
||||||
|
public rEvents: Event[];
|
||||||
|
|
||||||
|
@BelongsToMany(() => Group, () => GroupMember)
|
||||||
|
public rGroups: Group[];
|
||||||
|
|
||||||
|
@HasMany(() => Post, "authorId")
|
||||||
|
public rPosts: Post[];
|
||||||
|
|
||||||
|
@HasMany(() => Request, "senderId")
|
||||||
|
public rSentRequests: Request[];
|
||||||
|
|
||||||
|
@HasMany(() => Request, "receiverId")
|
||||||
|
public rReceivedRequests: Request[];
|
||||||
|
|
||||||
|
@HasMany(() => ChatMessage, "authorId")
|
||||||
|
public messages: ChatMessage[];
|
||||||
|
|
||||||
|
@HasMany(() => Group, "creatorId")
|
||||||
|
public rCreatedGroups: Group[];
|
||||||
|
|
||||||
|
@CreatedAt
|
||||||
|
public readonly createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdatedAt
|
||||||
|
public readonly updatedAt!: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the user
|
||||||
|
*/
|
||||||
|
public get name(): string {
|
||||||
|
return this.getDataValue("username");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The date the user joined the network
|
||||||
|
*/
|
||||||
|
public get joinedAt(): Date {
|
||||||
|
return this.getDataValue("createdAt");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The points of the user
|
||||||
|
*/
|
||||||
|
public get points(): number {
|
||||||
|
return this.rankpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The level of the user which is the points divided by 100
|
||||||
|
*/
|
||||||
|
public get level(): number {
|
||||||
|
return Math.ceil(this.rankpoints / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All friends of the user
|
||||||
|
* @param first
|
||||||
|
* @param offset
|
||||||
|
*/
|
||||||
|
public async friends({first, offset}: { first: number, offset: number }): Promise<User[]> {
|
||||||
|
const limit = first || 10;
|
||||||
|
offset = offset || 0;
|
||||||
|
return await this.$get("rFriendOf", {limit, offset}) as User[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of the users friends.
|
||||||
|
*/
|
||||||
|
public async friendCount(): Promise<number> {
|
||||||
|
return this.$count("rFriends");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The chats the user has joined
|
||||||
|
* @param first
|
||||||
|
* @param offset
|
||||||
|
*/
|
||||||
|
public async chats({first, offset}: { first: number, offset: number }): Promise<ChatRoom[]> {
|
||||||
|
const limit = first || 10;
|
||||||
|
offset = offset || 0;
|
||||||
|
return await this.$get("rChats", {limit, offset}) as ChatRoom[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the number of chats the user has
|
||||||
|
*/
|
||||||
|
public async chatCount(): Promise<number> {
|
||||||
|
return this.$count("rChats");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All active requests the user ha ssent
|
||||||
|
*/
|
||||||
|
public async sentRequests(): Promise<Request[]> {
|
||||||
|
return await this.$get("rSentRequests") as Request[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All requests the user has received
|
||||||
|
*/
|
||||||
|
public async receivedRequests(): Promise<Request[]> {
|
||||||
|
return await this.$get("rReceivedRequests") as Request[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async posts({first, offset}: { first: number, offset: number }): Promise<Post[]> {
|
||||||
|
const limit = first || 10;
|
||||||
|
offset = offset || 0;
|
||||||
|
return await this.$get("rPosts", {limit, offset}) as Post[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* use {@link postCount} instead
|
||||||
|
*/
|
||||||
|
public async numberOfPosts(): Promise<number> {
|
||||||
|
return this.postCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* number of posts the user created
|
||||||
|
*/
|
||||||
|
public async postCount(): Promise<number> {
|
||||||
|
return this.$count("rPosts");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups the user is the admin of
|
||||||
|
*/
|
||||||
|
public async administratedGroups(): Promise<Group[]> {
|
||||||
|
return await this.$get("rAdministratedGroups") as Group[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups the user has created
|
||||||
|
*/
|
||||||
|
public async createdGroups(): Promise<Group[]> {
|
||||||
|
return await this.$get("rCreatedGroups") as Group[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups the user is a member of
|
||||||
|
* @param first
|
||||||
|
* @param offset
|
||||||
|
*/
|
||||||
|
public async groups({first, offset}: { first: number, offset: number }): Promise<Group[]> {
|
||||||
|
const limit = first || 10;
|
||||||
|
offset = offset || 0;
|
||||||
|
return await this.$get("rGroups", {limit, offset}) as Group[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of groups the user has joined
|
||||||
|
*/
|
||||||
|
public async groupCount(): Promise<number> {
|
||||||
|
return this.$count("rGroups");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events the user has joined
|
||||||
|
*/
|
||||||
|
public async events(): Promise<Event[]> {
|
||||||
|
return await this.$get("rEvents") as Event[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of events the user is participating in.
|
||||||
|
*/
|
||||||
|
public async eventCount(): Promise<number> {
|
||||||
|
return this.$count("rEvents");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Denys a request the user has received
|
||||||
|
* @param sender
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
public async denyRequest(sender: number, type: RequestType) {
|
||||||
|
const request = await this.$get("rReceivedRequests",
|
||||||
|
{where: {senderId: sender, requestType: type}}) as Request[];
|
||||||
|
if (request[0]) {
|
||||||
|
await request[0].destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts a request the user has received
|
||||||
|
* @param sender
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
public async acceptRequest(sender: number, type: RequestType) {
|
||||||
|
const requests = await this.$get("rReceivedRequests",
|
||||||
|
{where: {senderId: sender, requestType: type}}) as Request[];
|
||||||
|
if (requests.length > 0) {
|
||||||
|
const request = requests[0];
|
||||||
|
if (request.requestType === RequestType.FRIENDREQUEST) {
|
||||||
|
await Friendship.bulkCreate([
|
||||||
|
{userId: this.id, friendId: sender},
|
||||||
|
{userId: sender, friendId: this.id},
|
||||||
|
], {ignoreDuplicates: true});
|
||||||
|
await request.destroy();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new RequestNotFoundError(sender, this.id, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a user from the users friends
|
||||||
|
* @param friendId
|
||||||
|
*/
|
||||||
|
public async removeFriend(friendId: number) {
|
||||||
|
const friend = await User.findByPk(friendId);
|
||||||
|
if (friend) {
|
||||||
|
await this.$remove("rFriends", friend);
|
||||||
|
await this.$remove("rFriendOf", friend);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new UserNotFoundError(friendId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
export {ChatMember} from "./ChatMember";
|
||||||
|
export {ChatMessage} from "./ChatMessage";
|
||||||
|
export {ChatRoom} from "./ChatRoom";
|
||||||
|
export {Friendship} from "./Friendship";
|
||||||
|
export {Post} from "./Post";
|
||||||
|
export {PostVote} from "./PostVote";
|
||||||
|
export {Request} from "./Request";
|
||||||
|
export {User} from "./User";
|
||||||
|
export {Group} from "./Group";
|
||||||
|
export {GroupAdmin} from "./GroupAdmin";
|
||||||
|
export {GroupMember} from "./GroupMember";
|
||||||
|
export {Event} from "./Event";
|
||||||
|
export {EventParticipant} from "./EventParticipant";
|
@ -1,11 +1,11 @@
|
|||||||
export namespace is {
|
export namespace is {
|
||||||
const emailRegex = /\S+?@\S+?(\.\S+?)?\.\w{2,3}(.\w{2-3})?/g;
|
const emailRegex = /\S+?@\S+?(\.\S+?)?\.\w{2,3}(.\w{2-3})?/g;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests if a string is a valid email.
|
* Tests if a string is a valid email.
|
||||||
* @param testString
|
* @param testString
|
||||||
*/
|
*/
|
||||||
export function email(testString: string) {
|
export function email(testString: string) {
|
||||||
return emailRegex.test(testString);
|
return emailRegex.test(testString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
@mixin gridPosition($rowStart, $rowEnd, $columnStart, $columnEnd)
|
|
||||||
grid-row-start: $rowStart
|
|
||||||
grid-row-end: $rowEnd
|
|
||||||
grid-column-start: $columnStart
|
|
||||||
grid-column-end: $columnEnd
|
|
@ -1,108 +1,10 @@
|
|||||||
@import "vars"
|
body
|
||||||
@import "mixins"
|
font-family: Arial, serif
|
||||||
|
|
||||||
body
|
#server-error
|
||||||
font-family: Arial, serif
|
*
|
||||||
|
margin-left: auto
|
||||||
button
|
margin-right: auto
|
||||||
border: 2px solid $cPrimary
|
text-align: center
|
||||||
margin-top: 0.125em
|
code
|
||||||
padding: 0.125em
|
font-size: 2em
|
||||||
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
|
|
||||||
|
|
||||||
#input-login
|
|
||||||
margin-top: 1em
|
|
||||||
@include gridPosition(2,3,2,3)
|
|
||||||
grid-template: 7.5% 7.5% 7.5% 7.5% 72%/ 100%
|
|
||||||
display: grid
|
|
||||||
background-color: $cPrimaryBackground
|
|
||||||
|
|
||||||
input
|
|
||||||
margin: 0.25em
|
|
||||||
|
|
||||||
.loginButton
|
|
||||||
margin: 0.25em
|
|
||||||
|
|
||||||
#input-register
|
|
||||||
margin-top: 1em
|
|
||||||
@include gridPosition(2,3,2,3)
|
|
||||||
grid-template: 7.5% 7.5% 7.5% 7.5% 7.5% 7.5% 58%/ 100%
|
|
||||||
display: grid
|
|
||||||
background-color: $cPrimaryBackground
|
|
||||||
|
|
||||||
input
|
|
||||||
margin: 0.25em
|
|
||||||
|
|
||||||
.registerButton
|
|
||||||
margin: 0.25em
|
|
||||||
|
|
||||||
#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
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
$cPrimaryBackground: #fff
|
|
||||||
$cSecondaryBackground: #ddd
|
|
||||||
$cInactiveText: #555
|
|
||||||
$cPrimary: #0d6b14
|
|
||||||
$cPrimarySurface: #fff
|
|
@ -1,101 +1,98 @@
|
|||||||
import {Router} from "express";
|
import {Router} from "express";
|
||||||
import {Namespace, Server} from "socket.io";
|
import {Namespace, Server} from "socket.io";
|
||||||
import dataaccess from "../lib/dataaccess";
|
import dataaccess from "../lib/dataaccess";
|
||||||
import {ChatMessage} from "../lib/dataaccess/ChatMessage";
|
import globals from "../lib/globals";
|
||||||
import {Chatroom} from "../lib/dataaccess/Chatroom";
|
import {InternalEvents} from "../lib/InternalEvents";
|
||||||
import {Post} from "../lib/dataaccess/Post";
|
import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/models";
|
||||||
import {Request} from "../lib/dataaccess/Request";
|
import Route from "../lib/Route";
|
||||||
import globals from "../lib/globals";
|
|
||||||
import {InternalEvents} from "../lib/InternalEvents";
|
/**
|
||||||
import Route from "../lib/Route";
|
* list of chatroom socket namespaces.
|
||||||
|
*/
|
||||||
/**
|
const chatRooms: Namespace[] = [];
|
||||||
* list of chatroom socket namespaces.
|
|
||||||
*/
|
/**
|
||||||
const chatRooms: Namespace[] = [];
|
* Class for the home route.
|
||||||
|
*/
|
||||||
/**
|
class HomeRoute extends Route {
|
||||||
* Class for the home route.
|
/**
|
||||||
*/
|
* Constructor, creates new router.
|
||||||
class HomeRoute extends Route {
|
*/
|
||||||
/**
|
constructor() {
|
||||||
* Constructor, creates new router.
|
super();
|
||||||
*/
|
this.router = Router();
|
||||||
constructor() {
|
}
|
||||||
super();
|
|
||||||
this.router = Router();
|
/**
|
||||||
}
|
* Asynchronous init for socket.io.
|
||||||
|
* @param io - the io instance
|
||||||
/**
|
*/
|
||||||
* Asynchronous init for socket.io.
|
public async init(io: Server) {
|
||||||
* @param io - the io instance
|
this.io = io;
|
||||||
*/
|
|
||||||
public async init(io: Server) {
|
io.on("connection", (socket) => {
|
||||||
this.io = io;
|
socket.on("postCreate", async (content) => {
|
||||||
|
if (socket.handshake.session.userId) {
|
||||||
io.on("connection", (socket) => {
|
const post = await dataaccess.createPost(content, socket.handshake.session.userId);
|
||||||
socket.on("postCreate", async (content) => {
|
io.emit("post", Object.assign(post, {htmlContent: post.htmlContent}));
|
||||||
if (socket.handshake.session.userId) {
|
} else {
|
||||||
const post = await dataaccess.createPost(content, socket.handshake.session.userId);
|
socket.emit("error", "Not logged in!");
|
||||||
io.emit("post", await post.resolvedData());
|
}
|
||||||
} else {
|
});
|
||||||
socket.emit("error", "Not logged in!");
|
globals.internalEmitter.on(InternalEvents.REQUESTCREATE, async (request: Request) => {
|
||||||
}
|
if ((await request.$get("sender") as User).id === socket.handshake.session.userId) {
|
||||||
});
|
socket.emit("request", request);
|
||||||
globals.internalEmitter.on(InternalEvents.REQUESTCREATE, (request: Request) => {
|
}
|
||||||
if (request.receiver.id === socket.handshake.session.userId) {
|
});
|
||||||
socket.emit("request", request.resolvedData());
|
globals.internalEmitter.on(InternalEvents.GQLPOSTCREATE, async (post: Post) => {
|
||||||
}
|
socket.emit("post", Object.assign(post, {htmlContent: post.htmlContent}));
|
||||||
});
|
});
|
||||||
globals.internalEmitter.on(InternalEvents.GQLPOSTCREATE, async (post: Post) => {
|
});
|
||||||
socket.emit("post", await post.resolvedData());
|
|
||||||
});
|
const chats = await dataaccess.getAllChats();
|
||||||
});
|
for (const chat of chats) {
|
||||||
|
chatRooms[chat.id] = this.getChatSocketNamespace(chat.id);
|
||||||
const chats = await dataaccess.getAllChats();
|
}
|
||||||
for (const chat of chats) {
|
globals.internalEmitter.on(InternalEvents.CHATCREATE, (chat: ChatRoom) => {
|
||||||
chatRooms[chat.id] = this.getChatSocketNamespace(chat.id);
|
chatRooms[chat.id] = this.getChatSocketNamespace(chat.id);
|
||||||
}
|
});
|
||||||
globals.internalEmitter.on(InternalEvents.CHATCREATE, (chat: Chatroom) => {
|
}
|
||||||
chatRooms[chat.id] = this.getChatSocketNamespace(chat.id);
|
|
||||||
});
|
/**
|
||||||
}
|
* Destroys the instance by dereferencing the router and resolver.
|
||||||
|
*/
|
||||||
/**
|
public async destroy(): Promise<void> {
|
||||||
* Destroys the instance by dereferencing the router and resolver.
|
this.router = null;
|
||||||
*/
|
}
|
||||||
public async destroy(): Promise<void> {
|
|
||||||
this.router = null;
|
/**
|
||||||
}
|
* Returns the namespace socket for a chat socket.
|
||||||
|
* @param chatId
|
||||||
/**
|
*/
|
||||||
* Returns the namespace socket for a chat socket.
|
private getChatSocketNamespace(chatId: number) {
|
||||||
* @param chatId
|
if (chatRooms[chatId]) {
|
||||||
*/
|
return chatRooms[chatId];
|
||||||
private getChatSocketNamespace(chatId: number) {
|
}
|
||||||
if (chatRooms[chatId]) {
|
const chatNs = this.io.of(`/chat/${chatId}`);
|
||||||
return chatRooms[chatId];
|
chatNs.on("connection", (socket) => {
|
||||||
}
|
socket.on("chatMessage", async (content) => {
|
||||||
const chatNs = this.io.of(`/chat/${chatId}`);
|
if (socket.handshake.session.userId) {
|
||||||
chatNs.on("connection", (socket) => {
|
const userId = socket.handshake.session.userId;
|
||||||
socket.on("chatMessage", async (content) => {
|
const message = await dataaccess.sendChatMessage(userId, chatId, content);
|
||||||
if (socket.handshake.session.userId) {
|
socket.broadcast.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent}));
|
||||||
const userId = socket.handshake.session.userId;
|
socket.emit("chatMessageSent", Object.assign(message, {htmlContent: message.htmlContent}));
|
||||||
const message = await dataaccess.sendChatMessage(userId, chatId, content);
|
} else {
|
||||||
socket.broadcast.emit("chatMessage", message.resolvedContent());
|
socket.emit("error", "Not logged in!");
|
||||||
socket.emit("chatMessageSent", message.resolvedContent());
|
}
|
||||||
} else {
|
});
|
||||||
socket.emit("error", "Not logged in!");
|
globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, async (message: ChatMessage) => {
|
||||||
}
|
if ((await message.$get("chat") as ChatRoom).id === chatId) {
|
||||||
});
|
socket.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent}));
|
||||||
globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, (message: ChatMessage) => {
|
}
|
||||||
if (message.chat.id === chatId) {
|
});
|
||||||
socket.emit("chatMessage", message.resolvedContent());
|
});
|
||||||
}
|
return chatNs;
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
return chatNs;
|
|
||||||
}
|
export default HomeRoute;
|
||||||
}
|
|
||||||
|
|
||||||
export default HomeRoute;
|
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
/**
|
/**
|
||||||
* @author Trivernis
|
* @author Trivernis
|
||||||
* @remarks
|
* @remarks
|
||||||
*
|
*
|
||||||
* Taken from {@link https://github.com/Trivernis/whooshy}
|
* Taken from {@link https://github.com/Trivernis/whooshy}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Router} from "express";
|
import {Router} from "express";
|
||||||
import {Server} from "socket.io";
|
import {Server} from "socket.io";
|
||||||
|
|
||||||
import HomeRoute from "./home";
|
import HomeRoute from "./home";
|
||||||
|
|
||||||
const homeRoute = new HomeRoute();
|
const homeRoute = new HomeRoute();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Namespace to manage the routes of the server.
|
* Namespace to manage the routes of the server.
|
||||||
* Allows easier assignments of graphql endpoints, socket.io connections and routers when
|
* Allows easier assignments of graphql endpoints, socket.io connections and routers when
|
||||||
* used with {@link Route}.
|
* used with {@link Route}.
|
||||||
*/
|
*/
|
||||||
namespace routes {
|
namespace routes {
|
||||||
export const router = Router();
|
export const router = Router();
|
||||||
|
|
||||||
router.use("/", homeRoute.router);
|
router.use("/", homeRoute.router);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assigns the io listeners or namespaces to the routes
|
* Assigns the io listeners or namespaces to the routes
|
||||||
* @param io
|
* @param io
|
||||||
*/
|
*/
|
||||||
export const ioListeners = async (io: Server) => {
|
export const ioListeners = async (io: Server) => {
|
||||||
await homeRoute.init(io);
|
await homeRoute.init(io);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
@ -1,137 +0,0 @@
|
|||||||
--create functions
|
|
||||||
DO $$BEGIN
|
|
||||||
|
|
||||||
IF NOT EXISTS(SELECT 1 from pg_proc WHERE proname = 'function_exists') THEN
|
|
||||||
CREATE FUNCTION function_exists(text) RETURNS boolean LANGUAGE plpgsql AS $BODY$
|
|
||||||
BEGIN
|
|
||||||
RETURN EXISTS(SELECT 1 from pg_proc WHERE proname = $1);
|
|
||||||
END $BODY$;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF NOT function_exists('type_exists') THEN
|
|
||||||
CREATE FUNCTION type_exists(text) RETURNS boolean LANGUAGE plpgsql AS $BODY$
|
|
||||||
BEGIN
|
|
||||||
RETURN EXISTS (SELECT 1 FROM pg_type WHERE typname = $1);
|
|
||||||
END $BODY$;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END$$;
|
|
||||||
|
|
||||||
--create types
|
|
||||||
DO $$ BEGIN
|
|
||||||
|
|
||||||
IF NOT type_exists('votetype') THEN
|
|
||||||
CREATE TYPE votetype AS enum ('DOWNVOTE', 'UPVOTE');
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF NOT type_exists('posttype') THEN
|
|
||||||
CREATE TYPE posttype AS enum ('MISC', 'ACTION', 'IMAGE', 'TEXT');
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF NOT type_exists('requesttype') THEN
|
|
||||||
CREATE TYPE requesttype AS enum ('FRIENDREQUEST');
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END$$;
|
|
||||||
|
|
||||||
-- create functions relying on types
|
|
||||||
|
|
||||||
DO $$ BEGIN
|
|
||||||
|
|
||||||
IF NOT function_exists('cast_to_votetype') THEN
|
|
||||||
CREATE FUNCTION cast_to_votetype(text) RETURNS votetype LANGUAGE plpgsql AS $BODY$
|
|
||||||
BEGIN
|
|
||||||
RETURN CASE WHEN $1::votetype IS NULL THEN 'UPVOTE' ELSE $1::votetype END;
|
|
||||||
END $BODY$;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF NOT function_exists('cast_to_posttype') THEN
|
|
||||||
CREATE FUNCTION cast_to_posttype(text) RETURNS posttype LANGUAGE plpgsql AS $BODY$
|
|
||||||
BEGIN
|
|
||||||
RETURN CASE WHEN $1::posttype IS NULL THEN 'MISC' ELSE $1::posttype END;
|
|
||||||
END $BODY$;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END$$;
|
|
||||||
|
|
||||||
-- create tables
|
|
||||||
DO $$ BEGIN
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "user_sessions" (
|
|
||||||
"sid" varchar NOT NULL,
|
|
||||||
"sess" json NOT NULL,
|
|
||||||
"expire" timestamp(6) NOT NULL,
|
|
||||||
PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE
|
|
||||||
) WITH (OIDS=FALSE);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name varchar(128) NOT NULL,
|
|
||||||
handle varchar(128) UNIQUE NOT NULL,
|
|
||||||
password varchar(1024) NOT NULL,
|
|
||||||
email varchar(128) UNIQUE NOT NULL,
|
|
||||||
greenpoints INTEGER DEFAULT 0,
|
|
||||||
joined_at TIMESTAMP DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS posts (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
upvotes INTEGER DEFAULT 0,
|
|
||||||
downvotes INTEGER DEFAULT 0,
|
|
||||||
created_at TIMESTAMP DEFAULT now(),
|
|
||||||
content text,
|
|
||||||
author SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
type posttype NOT NULL DEFAULT 'MISC'
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS votes (
|
|
||||||
user_id SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
item_id BIGSERIAL REFERENCES posts (id) ON DELETE CASCADE,
|
|
||||||
vote_type votetype DEFAULT 'DOWNVOTE',
|
|
||||||
PRIMARY KEY (user_id, item_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS events (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
time TIMESTAMP,
|
|
||||||
owner SERIAL REFERENCES users (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS event_members (
|
|
||||||
event BIGSERIAL REFERENCES events (id),
|
|
||||||
member SERIAL REFERENCES users (id),
|
|
||||||
PRIMARY KEY (event, member)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS chats (
|
|
||||||
id BIGSERIAL PRIMARY KEY
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
||||||
chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE,
|
|
||||||
author SERIAL REFERENCES users (id) ON DELETE SET NULL,
|
|
||||||
content VARCHAR(1024) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT now(),
|
|
||||||
PRIMARY KEY (chat, author, created_at)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS chat_members (
|
|
||||||
chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE,
|
|
||||||
member SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (chat, member)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_friends (
|
|
||||||
user_id SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
friend_id SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (user_id, friend_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS requests (
|
|
||||||
sender SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
receiver SERIAL REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
type requesttype DEFAULT 'FRIENDREQUEST',
|
|
||||||
PRIMARY KEY (sender, receiver, type)
|
|
||||||
);
|
|
||||||
|
|
||||||
END $$;
|
|
@ -1,19 +0,0 @@
|
|||||||
DO $$ BEGIN
|
|
||||||
|
|
||||||
ALTER TABLE IF EXISTS votes
|
|
||||||
ADD COLUMN IF NOT EXISTS vote_type votetype DEFAULT 'UPVOTE',
|
|
||||||
ALTER COLUMN vote_type TYPE votetype USING cast_to_votetype(vote_type::text),
|
|
||||||
ALTER COLUMN vote_type DROP DEFAULT,
|
|
||||||
ALTER COLUMN vote_type SET DEFAULT 'UPVOTE';
|
|
||||||
|
|
||||||
ALTER TABLE IF EXISTS posts
|
|
||||||
ALTER COLUMN type TYPE posttype USING cast_to_posttype(type::text),
|
|
||||||
ALTER COLUMN type DROP DEFAULT,
|
|
||||||
ALTER COLUMN type SET DEFAULT 'MISC',
|
|
||||||
DROP COLUMN IF EXISTS upvotes,
|
|
||||||
DROP COLUMN IF EXISTS downvotes;
|
|
||||||
|
|
||||||
ALTER TABLE requests
|
|
||||||
ADD COLUMN IF NOT EXISTS type requesttype DEFAULT 'FRIENDREQUEST';
|
|
||||||
|
|
||||||
END $$;
|
|
@ -0,0 +1,12 @@
|
|||||||
|
html
|
||||||
|
head
|
||||||
|
link(href="/stylesheets/style.css" rel="stylesheet" type="text/css")
|
||||||
|
body
|
||||||
|
div#server-error
|
||||||
|
div
|
||||||
|
h1 Page not found!
|
||||||
|
div
|
||||||
|
code 404
|
||||||
|
div
|
||||||
|
h1 The page "#{url}" was not found.
|
||||||
|
|
@ -0,0 +1,13 @@
|
|||||||
|
html
|
||||||
|
head
|
||||||
|
link(href="/stylesheets/style.css" rel="stylesheet" type="text/css")
|
||||||
|
body
|
||||||
|
div#server-error
|
||||||
|
div
|
||||||
|
h1 Internal server error!
|
||||||
|
div
|
||||||
|
code 500
|
||||||
|
div
|
||||||
|
h2 Oops the server couldn't handle that.
|
||||||
|
div
|
||||||
|
p You might want to report this.
|
@ -1,21 +1,24 @@
|
|||||||
{
|
{
|
||||||
"compileOnSave": true,
|
"compileOnSave": true,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"preserveConstEnums": true,
|
"preserveConstEnums": true,
|
||||||
"outDir": "./dist",
|
"allowSyntheticDefaultImports": true,
|
||||||
"sourceMap": true,
|
"outDir": "./dist",
|
||||||
"target": "es2018",
|
"sourceMap": true,
|
||||||
"allowJs": true,
|
"target": "es2018",
|
||||||
"moduleResolution": "node",
|
"allowJs": true,
|
||||||
"module": "commonjs"
|
"moduleResolution": "node",
|
||||||
},
|
"module": "commonjs",
|
||||||
"include": [
|
"experimentalDecorators": true,
|
||||||
"src/**/*"
|
"emitDecoratorMetadata": true
|
||||||
],
|
},
|
||||||
"exclude": [
|
"include": [
|
||||||
"node_modules",
|
"src/**/*"
|
||||||
"**/*.spec.ts"
|
],
|
||||||
]
|
"exclude": [
|
||||||
}
|
"node_modules",
|
||||||
|
"**/*.spec.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
@ -1,31 +1,32 @@
|
|||||||
{
|
{
|
||||||
"extends": "tslint:recommended",
|
"extends": "tslint:recommended",
|
||||||
"rulesDirectory": [],
|
"rulesDirectory": [],
|
||||||
"rules": {
|
"rules": {
|
||||||
"max-line-length": {
|
"max-line-length": {
|
||||||
"options": [120]
|
"options": [120]
|
||||||
},
|
},
|
||||||
"new-parens": true,
|
"new-parens": true,
|
||||||
"no-arg": true,
|
"no-arg": true,
|
||||||
"no-bitwise": true,
|
"no-bitwise": true,
|
||||||
"no-conditional-assignment": true,
|
"no-conditional-assignment": true,
|
||||||
"no-consecutive-blank-lines": false,
|
"no-consecutive-blank-lines": false,
|
||||||
"cyclomatic-complexity": true,
|
"cyclomatic-complexity": true,
|
||||||
"brace-style": "1tbs",
|
"brace-style": "1tbs",
|
||||||
"semicolon": true,
|
"semicolon": true,
|
||||||
"indent": [true, "spaces", 4],
|
"indent": [true, "spaces", 4],
|
||||||
"no-shadowed-variable": true,
|
"no-shadowed-variable": true,
|
||||||
"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-namespace": false,
|
||||||
"no-internal-module": false,
|
"no-internal-module": false,
|
||||||
"max-classes-per-file": false
|
"max-classes-per-file": false,
|
||||||
},
|
"no-var-requires": false
|
||||||
"jsRules": {
|
},
|
||||||
"max-line-length": {
|
"jsRules": {
|
||||||
"options": [120]
|
"max-line-length": {
|
||||||
}
|
"options": [120]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue