Data access improvements

- fixed votes
- added column constraints
- added post not found error to vote
pull/2/head
Trivernis 5 years ago
parent 298940cc32
commit c68f11080f

1
.gitignore vendored

@ -7,3 +7,4 @@ dist
.idea .idea
config.yaml config.yaml
sqz-force sqz-force
greenvironment.db

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

@ -1,8 +1,8 @@
import {GraphQLError} from "graphql"; import {GraphQLError} from "graphql";
import * as status from "http-status"; import * as status from "http-status";
import dataaccess from "../lib/dataaccess"; import dataaccess from "../lib/dataaccess";
import * as models from "../lib/dataaccess/models"; import * as models from "../lib/models";
import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors"; import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors";
import globals from "../lib/globals"; import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents"; import {InternalEvents} from "../lib/InternalEvents";
import {is} from "../lib/regex"; import {is} from "../lib/regex";
@ -103,7 +103,12 @@ export function resolver(req: any, res: any): any {
if (postId && type) { if (postId && type) {
if (req.session.userId) { if (req.session.userId) {
const post = await models.Post.findByPk(postId); const post = await models.Post.findByPk(postId);
return await post.vote(req.session.userId, type); if (post) {
return await post.vote(req.session.userId, type);
} else {
res.status(400);
return new PostNotFoundGqlError(postId);
}
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();

@ -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,9 +1,9 @@
import {Sequelize} from "sequelize-typescript"; import {Sequelize} from "sequelize-typescript";
import {ChatNotFoundError} from "../errors/ChatNotFoundError"; import {ChatNotFoundError} from "./errors/ChatNotFoundError";
import {EmailAlreadyRegisteredError} from "../errors/EmailAlreadyRegisteredError"; import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError";
import {UserNotFoundError} from "../errors/UserNotFoundError"; import {UserNotFoundError} from "./errors/UserNotFoundError";
import globals from "../globals"; import globals from "./globals";
import {InternalEvents} from "../InternalEvents"; import {InternalEvents} from "./InternalEvents";
import * as models from "./models"; import * as models from "./models";
/** /**

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

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

@ -1,14 +1,16 @@
import {Column, ForeignKey, Model, Table,} from "sequelize-typescript"; import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import {ChatRoom} from "./ChatRoom"; import {ChatRoom} from "./ChatRoom";
import {User} from "./User"; import {User} from "./User";
@Table({underscored: true}) @Table({underscored: true})
export class ChatMember extends Model<ChatMember> { export class ChatMember extends Model<ChatMember> {
@ForeignKey(() => User) @ForeignKey(() => User)
@Column @NotNull
@Column({allowNull: false})
public userId: number; public userId: number;
@ForeignKey(() => ChatRoom) @ForeignKey(() => ChatRoom)
@Column @NotNull
@Column({allowNull: false})
public chatId: number; public chatId: number;
} }

@ -1,21 +1,24 @@
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {BelongsTo, Column, CreatedAt, ForeignKey, Model, Table,} from "sequelize-typescript"; import {BelongsTo, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import markdown from "../../markdown"; import markdown from "../markdown";
import {ChatRoom} from "./ChatRoom"; import {ChatRoom} from "./ChatRoom";
import {User} from "./User"; import {User} from "./User";
@Table({underscored: true}) @Table({underscored: true})
export class ChatMessage extends Model<ChatMessage> { export class ChatMessage extends Model<ChatMessage> {
@Column(sqz.STRING(512)) @NotNull
@Column({type: sqz.STRING(512), allowNull: false})
public content: string; public content: string;
@ForeignKey(() => ChatRoom) @ForeignKey(() => ChatRoom)
@Column @NotNull
@Column({allowNull: false})
public chatId: number; public chatId: number;
@ForeignKey(() => User) @ForeignKey(() => User)
@Column @NotNull
@Column({allowNull: false})
public authorId: number; public authorId: number;
@BelongsTo(() => ChatRoom, "chatId") @BelongsTo(() => ChatRoom, "chatId")

@ -1,14 +1,16 @@
import {Column, ForeignKey, Model, Table} from "sequelize-typescript"; import {Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import {User} from "./User"; import {User} from "./User";
@Table({underscored: true}) @Table({underscored: true})
export class Friendship extends Model<Friendship> { export class Friendship extends Model<Friendship> {
@ForeignKey(() => User) @ForeignKey(() => User)
@Column @NotNull
@Column({allowNull: false})
public userId: number; public userId: number;
@ForeignKey(() => User) @ForeignKey(() => User)
@Column @NotNull
@Column({allowNull: false})
public friendId: number; public friendId: number;
} }

@ -1,16 +1,18 @@
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, Table,} from "sequelize-typescript"; import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import markdown from "../../markdown"; import markdown from "../markdown";
import {PostVote, VoteType} from "./PostVote"; import {PostVote, VoteType} from "./PostVote";
import {User} from "./User"; import {User} from "./User";
@Table({underscored: true}) @Table({underscored: true})
export class Post extends Model<Post> { export class Post extends Model<Post> {
@Column(sqz.STRING(2048)) @NotNull
@Column({type: sqz.STRING(2048), allowNull: false})
public content: string; public content: string;
@ForeignKey(() => User) @ForeignKey(() => User)
@Column @NotNull
@Column({allowNull: false})
public authorId: number; public authorId: number;
@BelongsTo(() => User, "authorId") @BelongsTo(() => User, "authorId")
@ -44,21 +46,25 @@ export class Post extends Model<Post> {
public async vote(userId: number, type: VoteType): Promise<VoteType> { public async vote(userId: number, type: VoteType): Promise<VoteType> {
type = type || VoteType.UPVOTE; type = type || VoteType.UPVOTE;
let vote = await PostVote.findOne({where: {user_id: userId, post_id: this.id}}); let votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>;
let vote = votes[0] || null;
let created = false;
if (!vote) { if (!vote) {
await this.$add("rVotes", userId); await this.$add("rVote", userId);
vote = await PostVote.findOne({where: {user_id: userId, post_id: this.id}}); votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>;
vote = votes[0] || null;
created = true;
} }
if (vote) { if (vote) {
if (vote.voteType === type) { if (vote.PostVote.voteType === type && !created) {
await vote.destroy(); await vote.PostVote.destroy();
return null; return null;
} else { } else {
vote.voteType = type; vote.PostVote.voteType = type;
await vote.save(); await vote.PostVote.save();
} }
} }
return vote.voteType; return vote.PostVote.voteType;
} }
} }

@ -1,5 +1,5 @@
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {Column, ForeignKey, Model, Table,} from "sequelize-typescript"; import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import {Post} from "./Post"; import {Post} from "./Post";
import {User} from "./User"; import {User} from "./User";
@ -10,14 +10,17 @@ export enum VoteType {
@Table({underscored: true}) @Table({underscored: true})
export class PostVote extends Model<PostVote> { export class PostVote extends Model<PostVote> {
@Column({type: sqz.ENUM, values: ["UPVOTE", "DOWNVOTE"]}) @NotNull
@Column({type: sqz.ENUM, values: ["UPVOTE", "DOWNVOTE"], defaultValue: "UPVOTE", allowNull: false})
public voteType: VoteType; public voteType: VoteType;
@ForeignKey(() => User) @ForeignKey(() => User)
@Column @NotNull
@Column({allowNull: false})
public userId: number; public userId: number;
@ForeignKey(() => Post) @ForeignKey(() => Post)
@Column @NotNull
@Column({allowNull: false})
public postId: number; public postId: number;
} }

@ -1,5 +1,5 @@
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {BelongsTo, Column, ForeignKey, Model, Table,} from "sequelize-typescript"; import {BelongsTo, Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import {User} from "./User"; import {User} from "./User";
export enum RequestType { export enum RequestType {
@ -10,18 +10,22 @@ export enum RequestType {
@Table({underscored: true}) @Table({underscored: true})
export class Request extends Model<Request> { export class Request extends Model<Request> {
@Column({type: sqz.ENUM, values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"]}) @NotNull
@Column({type: sqz.ENUM, values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"],
defaultValue: "FRIENDREQUEST", allowNull: false})
public requestType: RequestType; public requestType: RequestType;
@ForeignKey(() => User) @ForeignKey(() => User)
@Column @NotNull
@Column({allowNull: false})
public senderId: number; public senderId: number;
@BelongsTo(() => User, "senderId") @BelongsTo(() => User, "senderId")
public rSender: User; public rSender: User;
@ForeignKey(() => User) @ForeignKey(() => User)
@Column @NotNull
@Column({allowNull: false})
public receiverId: number; public receiverId: number;
@BelongsTo(() => User, "receiverId") @BelongsTo(() => User, "receiverId")

@ -1,6 +1,16 @@
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {BelongsToMany, Column, CreatedAt, HasMany, Model, Table, UpdatedAt,} from "sequelize-typescript"; import {
import {RequestNotFoundError} from "../../errors/RequestNotFoundError"; BelongsToMany,
Column,
CreatedAt,
HasMany,
Model,
NotNull,
Table,
Unique,
UpdatedAt,
} from "sequelize-typescript";
import {RequestNotFoundError} from "../errors/RequestNotFoundError";
import {ChatMember} from "./ChatMember"; import {ChatMember} from "./ChatMember";
import {ChatMessage} from "./ChatMessage"; import {ChatMessage} from "./ChatMessage";
import {ChatRoom} from "./ChatRoom"; import {ChatRoom} from "./ChatRoom";
@ -11,19 +21,26 @@ import {Request, RequestType} from "./Request";
@Table({underscored: true}) @Table({underscored: true})
export class User extends Model<User> { export class User extends Model<User> {
@Column(sqz.STRING(128)) @NotNull
@Column({type: sqz.STRING(128), allowNull: false})
public username: string; public username: string;
@Column(sqz.STRING(128)) @NotNull
@Unique
@Column({type: sqz.STRING(128), allowNull: false, unique: true})
public handle: string; public handle: string;
@Column(sqz.STRING(128)) @Unique
@NotNull
@Column({type: sqz.STRING(128), allowNull: false, unique: true})
public email: string; public email: string;
@Column(sqz.STRING(128)) @NotNull
@Column({type: sqz.STRING(128), allowNull: false})
public password: string; public password: string;
@Column({defaultValue: 0}) @NotNull
@Column({defaultValue: 0, allowNull: false})
public rankpoints: number; public rankpoints: number;
@BelongsToMany(() => User, () => Friendship) @BelongsToMany(() => User, () => Friendship)

@ -1,7 +1,7 @@
import {Router} from "express"; import {Router} from "express";
import {Namespace, Server} from "socket.io"; import {Namespace, Server} from "socket.io";
import dataaccess from "../lib/dataaccess"; import dataaccess from "../lib/dataaccess";
import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/dataaccess/models"; import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/models";
import globals from "../lib/globals"; import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents"; import {InternalEvents} from "../lib/InternalEvents";
import Route from "../lib/Route"; import Route from "../lib/Route";

@ -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 $$;
Loading…
Cancel
Save