Added Sequelize
- added sequelize for database management - added sequelize models under dataaccess/datamodels - removed postgres-only stuffpull/2/head
parent
b671845337
commit
57338355a7
File diff suppressed because it is too large
Load Diff
@ -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,31 +1,38 @@
|
||||
import markdown from "../markdown";
|
||||
import {Chatroom} from "./Chatroom";
|
||||
import * as models from "./datamodels/models";
|
||||
import {User} from "./User";
|
||||
|
||||
export class ChatMessage {
|
||||
constructor(
|
||||
public readonly author: User,
|
||||
public readonly chat: Chatroom,
|
||||
public readonly createdAt: number,
|
||||
public readonly content: string) {}
|
||||
|
||||
public id: number;
|
||||
public content: string;
|
||||
public createdAt: Date;
|
||||
|
||||
constructor(private message: models.ChatMessage) {
|
||||
this.id = message.id;
|
||||
this.content = message.content;
|
||||
this.createdAt = message.createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the author of the chat message.
|
||||
*/
|
||||
public async author(): Promise<User> {
|
||||
return new User(await this.message.getAuthor());
|
||||
}
|
||||
|
||||
/**
|
||||
* The content rendered by markdown-it.
|
||||
* Returns the rendered html content of the chat message.
|
||||
*/
|
||||
public htmlContent(): string {
|
||||
return markdown.renderInline(this.content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns resolved and rendered content of the chat message.
|
||||
* returns the chatroom for the chatmessage.
|
||||
*/
|
||||
public resolvedContent() {
|
||||
return {
|
||||
author: this.author.id,
|
||||
chat: this.chat.id,
|
||||
content: this.content,
|
||||
createdAt: this.createdAt,
|
||||
htmlContent: this.htmlContent(),
|
||||
};
|
||||
public async chat(): Promise<Chatroom> {
|
||||
return (await this.message.getChat()).chatroom;
|
||||
}
|
||||
}
|
||||
|
@ -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,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,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
export {
|
||||
init as datainit,
|
||||
User as SqUser,
|
||||
Post as SqPost,
|
||||
Chat as SqChat,
|
||||
Request as SqRequest,
|
||||
PostVotes as SqPostVotes,
|
||||
ChatMessage as SqChatMessage,
|
||||
ChatMembers as SqChatMembers,
|
||||
RequestType as SqRequestType,
|
||||
UserFriends as SqUserFriends,
|
||||
} from "./models";
|
@ -0,0 +1,279 @@
|
||||
// tslint:disable:object-literal-sort-keys
|
||||
|
||||
import * as sqz from "sequelize";
|
||||
import {
|
||||
Association,
|
||||
BelongsToGetAssociationMixin,
|
||||
BelongsToManyAddAssociationMixin,
|
||||
BelongsToManyCountAssociationsMixin,
|
||||
BelongsToManyCreateAssociationMixin,
|
||||
BelongsToManyGetAssociationsMixin,
|
||||
BelongsToManyHasAssociationMixin,
|
||||
DataTypes,
|
||||
HasManyAddAssociationMixin,
|
||||
HasManyCountAssociationsMixin,
|
||||
HasManyCreateAssociationMixin,
|
||||
HasManyGetAssociationsMixin,
|
||||
HasManyHasAssociationMixin,
|
||||
HasOneGetAssociationMixin,
|
||||
Model,
|
||||
Sequelize,
|
||||
} from "sequelize";
|
||||
import * as wrappers from "../wrappers";
|
||||
|
||||
const underscored = true;
|
||||
|
||||
enum VoteType {
|
||||
UPVOTE = "UPVOTE",
|
||||
DOWNVOTE = "DOWNVOTE",
|
||||
}
|
||||
|
||||
export enum RequestType {
|
||||
FRIENDREQUEST = "FRIENDREQUEST",
|
||||
GROUPINVITE = "GROUPINVITE",
|
||||
EVENTINVITE = "EVENTINVITE",
|
||||
}
|
||||
|
||||
export class User extends Model {
|
||||
|
||||
public static associations: {
|
||||
friends: Association<User, User>;
|
||||
posts: Association<User, Post>;
|
||||
votes: Association<User, PostVotes>;
|
||||
requests: Association<User, Request>;
|
||||
};
|
||||
|
||||
public id!: number;
|
||||
public username!: string;
|
||||
public handle!: string;
|
||||
public email!: string;
|
||||
public password!: string;
|
||||
public rankpoints!: number;
|
||||
|
||||
public readonly createdAt!: Date;
|
||||
public readonly updatedAt!: Date;
|
||||
|
||||
public getFriends!: HasManyGetAssociationsMixin<User>;
|
||||
public addFriend!: HasManyAddAssociationMixin<User, number>;
|
||||
public hasFriend!: HasManyHasAssociationMixin<User, number>;
|
||||
public countFriends!: HasManyCountAssociationsMixin;
|
||||
|
||||
public getPosts!: HasManyGetAssociationsMixin<Post>;
|
||||
public addPost!: HasManyAddAssociationMixin<Post, number>;
|
||||
public hasPost!: HasManyHasAssociationMixin<Post, number>;
|
||||
public countPosts!: HasManyCountAssociationsMixin;
|
||||
public createPost!: HasManyCreateAssociationMixin<Post>;
|
||||
|
||||
public getReceivedRequests!: HasManyGetAssociationsMixin<Request>;
|
||||
public addReceivedRequest!: HasManyAddAssociationMixin<Request, number>;
|
||||
public hasReceivedRequest!: HasManyHasAssociationMixin<Request, number>;
|
||||
public countReceivedRequests!: HasManyCountAssociationsMixin;
|
||||
public createReceivedRequest!: HasManyCreateAssociationMixin<Request>;
|
||||
|
||||
|
||||
public getSentRequests!: HasManyGetAssociationsMixin<Request>;
|
||||
public addSentRequest!: HasManyAddAssociationMixin<Request, number>;
|
||||
public hasSentRequest!: HasManyHasAssociationMixin<Request, number>;
|
||||
public countSentRequests!: HasManyCountAssociationsMixin;
|
||||
public createSentRequest!: HasManyCreateAssociationMixin<Request>;
|
||||
|
||||
public getChats!: BelongsToManyGetAssociationsMixin<Chat>;
|
||||
public addChat!: BelongsToManyAddAssociationMixin<Chat, number>;
|
||||
public hasChat!: BelongsToManyHasAssociationMixin<Chat, number>;
|
||||
public countChats!: BelongsToManyCountAssociationsMixin;
|
||||
public createChat!: BelongsToManyCreateAssociationMixin<Chat>;
|
||||
|
||||
/**
|
||||
* Getter for joined at as the date the entry was created.
|
||||
*/
|
||||
public get joinedAt(): Date {
|
||||
// @ts-ignore
|
||||
return this.getDataValue("createdAt");
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps itself into a user
|
||||
*/
|
||||
public get user(): wrappers.User {
|
||||
return new wrappers.User(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the username.
|
||||
*/
|
||||
public get name(): string {
|
||||
return this.getDataValue("username");
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps itself into a profile.
|
||||
*/
|
||||
public get profile(): wrappers.Profile {
|
||||
return new wrappers.Profile(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class UserFriends extends Model {
|
||||
}
|
||||
|
||||
export class Post extends Model {
|
||||
|
||||
public static associations: {
|
||||
author: Association<Post, User>,
|
||||
votes: Association<Post, PostVotes>,
|
||||
};
|
||||
|
||||
public id!: number;
|
||||
public content!: string;
|
||||
|
||||
public readonly createdAt!: Date;
|
||||
public readonly updatedAt!: Date;
|
||||
|
||||
public getUser!: BelongsToGetAssociationMixin<User>;
|
||||
|
||||
public getVotes!: HasManyGetAssociationsMixin<PostVotes>;
|
||||
public addVote!: HasManyAddAssociationMixin<PostVotes, number>;
|
||||
public hasVote!: HasManyHasAssociationMixin<PostVotes, number>;
|
||||
public countVotes!: HasManyCountAssociationsMixin;
|
||||
public createVote!: HasManyCreateAssociationMixin<PostVotes>;
|
||||
|
||||
/**
|
||||
* Wraps itself into a Post instance.
|
||||
*/
|
||||
public get post(): wrappers.Post {
|
||||
return new wrappers.Post(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class PostVotes extends Model {
|
||||
public voteType: VoteType;
|
||||
}
|
||||
|
||||
export class Request extends Model {
|
||||
public id!: number;
|
||||
public requestType!: RequestType;
|
||||
|
||||
public getSender!: HasOneGetAssociationMixin<User>;
|
||||
public getReceiver!: HasOneGetAssociationMixin<User>;
|
||||
}
|
||||
|
||||
export class Chat extends Model {
|
||||
public static associations: {
|
||||
members: Association<Chat, User>,
|
||||
messages: Association<Chat, ChatMessage>,
|
||||
};
|
||||
|
||||
public id!: number;
|
||||
|
||||
public readonly createdAt!: Date;
|
||||
public readonly updatedAt!: Date;
|
||||
|
||||
public getMembers!: BelongsToManyGetAssociationsMixin<User>;
|
||||
public addMember!: BelongsToManyAddAssociationMixin<User, number>;
|
||||
public hasMember!: BelongsToManyHasAssociationMixin<User, number>;
|
||||
public countMembers!: BelongsToManyCountAssociationsMixin;
|
||||
|
||||
public getMessages!: HasManyGetAssociationsMixin<ChatMessage>;
|
||||
public addMessage!: HasManyAddAssociationMixin<ChatMessage, number>;
|
||||
public hasMessage!: HasManyHasAssociationMixin<ChatMessage, number>;
|
||||
public countMessages!: HasManyCountAssociationsMixin;
|
||||
public createMessage!: HasManyCreateAssociationMixin<ChatMessage>;
|
||||
|
||||
/**
|
||||
* wraps itself into a chatroom.
|
||||
*/
|
||||
public get chatroom(): wrappers.Chatroom {
|
||||
return new wrappers.Chatroom(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatMembers extends Model {
|
||||
}
|
||||
|
||||
export class ChatMessage extends Model {
|
||||
public id: number;
|
||||
public content!: string;
|
||||
public readonly createdAt!: Date;
|
||||
public readonly updatedAt!: Date;
|
||||
|
||||
public getAuthor!: BelongsToGetAssociationMixin<User>;
|
||||
public getChat!: BelongsToGetAssociationMixin<Chat>;
|
||||
|
||||
public get message(): wrappers.ChatMessage {
|
||||
return new wrappers.ChatMessage(this);
|
||||
}
|
||||
}
|
||||
|
||||
export function init(sequelize: Sequelize) {
|
||||
User.init({
|
||||
username: {
|
||||
allowNull: false,
|
||||
type: sqz.STRING(128),
|
||||
},
|
||||
handle: {
|
||||
allowNull: false,
|
||||
type: sqz.STRING(128),
|
||||
unique: true,
|
||||
},
|
||||
email: {
|
||||
allowNull: false,
|
||||
type: sqz.STRING(128),
|
||||
unique: true,
|
||||
},
|
||||
password: {
|
||||
allowNull: false,
|
||||
type: sqz.STRING(128),
|
||||
},
|
||||
rankpoints: {
|
||||
allowNull: false,
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
},
|
||||
}, {sequelize, underscored});
|
||||
|
||||
UserFriends.init({}, {sequelize, underscored});
|
||||
|
||||
Post.init({
|
||||
content: DataTypes.TEXT,
|
||||
}, {sequelize, underscored});
|
||||
|
||||
PostVotes.init({
|
||||
voteType: {
|
||||
type: DataTypes.ENUM,
|
||||
values: ["UPVOTE", "DOWNVOTE"],
|
||||
},
|
||||
}, {sequelize, underscored});
|
||||
|
||||
Request.init({
|
||||
requestType: {
|
||||
type: DataTypes.ENUM,
|
||||
values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"],
|
||||
},
|
||||
}, {sequelize, underscored});
|
||||
|
||||
Chat.init({}, {sequelize, underscored});
|
||||
|
||||
ChatMembers.init({}, {sequelize, underscored});
|
||||
|
||||
ChatMessage.init({
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {sequelize, underscored});
|
||||
|
||||
User.belongsToMany(User, {through: UserFriends, as: "friends"});
|
||||
Post.belongsTo(User, {foreignKey: "userId"});
|
||||
User.hasMany(Post, {as: "posts", foreignKey: "userId"});
|
||||
Post.belongsToMany(User, {through: PostVotes, as: "votes"});
|
||||
User.belongsToMany(Post, {through: PostVotes, as: "votes"});
|
||||
User.hasMany(Request, {as: "sentRequests"});
|
||||
User.hasMany(Request, {as: "receivedRequests"});
|
||||
User.belongsToMany(Chat, {through: ChatMembers});
|
||||
Chat.belongsToMany(User, {through: ChatMembers, as: "members"});
|
||||
Chat.hasMany(ChatMessage, {as: "messages"});
|
||||
ChatMessage.belongsTo(Chat);
|
||||
ChatMessage.belongsTo(User, {as: "author", foreignKey: "userId"});
|
||||
User.hasMany(ChatMessage, {foreignKey: "userId"});
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
export {User} from "./User";
|
||||
export {Chatroom} from "./Chatroom";
|
||||
export {Post} from "./Post";
|
||||
export {Profile} from "./Profile";
|
||||
export {ChatMessage} from "./ChatMessage";
|
Loading…
Reference in New Issue