diff --git a/package-lock.json b/package-lock.json index 45b28c0..7baeb51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1039,9 +1039,9 @@ } }, "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, "requires": { "anymatch": "^2.0.0", @@ -1858,9 +1858,9 @@ } }, "es5-ext": { - "version": "0.10.50", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", - "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==", + "version": "0.10.51", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.51.tgz", + "integrity": "sha512-oRpWzM2WcLHVKpnrcyB7OW8j/s67Ba04JCm0WnNv3RiABSvs7mrQlutB8DBv793gKcp0XENR8Il8WxGTlZ73gQ==", "dev": true, "requires": { "es6-iterator": "~2.0.3", @@ -1880,13 +1880,13 @@ } }, "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.2.tgz", + "integrity": "sha512-/ZypxQsArlv+KHpGvng52/Iz8by3EQPxhmbuz8yFG89N/caTFBSbcXONDw0aMjy827gQg26XAjP4uXFvnfINmQ==", "dev": true, "requires": { - "d": "1", - "es5-ext": "~0.10.14" + "d": "^1.0.1", + "es5-ext": "^0.10.51" } }, "es6-weak-map": { @@ -3021,6 +3021,11 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "g": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/g/-/g-2.0.1.tgz", + "integrity": "sha1-C1lj69DKcOO8jGdmk0oCGCHIuFc=" + }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -7008,9 +7013,9 @@ } }, "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true }, "uri-js": { diff --git a/package.json b/package.json index 8043e04..0fb8448 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "express-session": "^1.16.2", "express-socket.io-session": "^1.3.5", "fs-extra": "^8.1.0", + "g": "^2.0.1", "graphql": "^14.4.2", "graphql-import": "^0.7.1", "js-yaml": "^3.13.1", diff --git a/src/app.ts b/src/app.ts index 698a895..3634256 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,7 +2,7 @@ import * as express from "express"; import * as http from "http"; import * as path from "path"; import * as socketIo from "socket.io"; -import {DTO} from "./lib/DTO"; +import dataaccess from "./lib/dataaccess"; import globals from "./lib/globals"; import routes from "./routes"; @@ -10,20 +10,18 @@ class App { public app: express.Application; public io: socketIo.Server; public server: http.Server; - public dto: DTO; constructor() { this.app = express(); this.server = new http.Server(this.app); this.io = socketIo(this.server); - this.dto = new DTO(); } /** * initializes everything that needs to be initialized asynchronous. */ public async init() { - await this.dto.init(); + await dataaccess.init(); await routes.ioListeners(this.io); this.app.set("views", path.join(__dirname, "views")); this.app.set("view engine", "pug"); diff --git a/src/lib/DTO.ts b/src/lib/DTO.ts deleted file mode 100644 index 2d93484..0000000 --- a/src/lib/DTO.ts +++ /dev/null @@ -1,248 +0,0 @@ -import {Runtime} from "inspector"; -import {Pool} from "pg"; -import globals from "./globals"; -import {QueryHelper} from "./QueryHelper"; - -const config = globals.config; -const tableCreationFile = __dirname + "/../sql/create-tables.sql"; -const dbClient: Pool = new Pool({ - database: config.database.database, - host: config.database.host, - password: config.database.password, - port: config.database.port, - user: config.database.user, -}); -const queryHelper = new QueryHelper(dbClient, tableCreationFile); - -export class DTO { - private queryHelper: QueryHelper; - - constructor() { - this.queryHelper = queryHelper; - } - - /** - * Initializes everything that needs to be initialized asynchronous. - */ - public async init() { - await this.queryHelper.createTables(); - } - - /** - * Returns the user by id - * @param userId - */ - public getUser(userId: number) { - return new User(userId); - } - - /** - * Returns the user by handle. - * @param userHandle - */ - public async getUserByHandle(userHandle: string) { - const result = await this.queryHelper.first({ - text: "SELECT * FROM users WHERE users.handle = $1", - values: [userHandle], - }); - return new User(result.id, result); - } -} - -export class User { - public readonly id: number; - private $name: string; - private $handle: string; - private $email: string; - private $greenpoints: number; - private $joinedAt: string; - private dataLoaded: boolean; - - /** - * Constructor of the user - * @param id - * @param row - */ - constructor(id: number, private row?: any) { - this.id = id; - } - - /** - * The name of the user - */ - public async name(): Promise { - if (!this.dataLoaded) { - await this.loadData(); - } - return this.$name; - } - - /** - * Sets the username of the user - * @param name - */ - public async setName(name: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE TABLE users SET name = $1 WHERE id = $2", - values: [name, this.id], - }); - return result.name; - } - - /** - * The unique handle of the user. - */ - public async handle(): Promise { - if (!this.dataLoaded) { - await this.loadData(); - } - return this.$handle; - } - - /** - * Updates the handle of the user - */ - public async setHandle(handle: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE TABLE users SET handle = $1 WHERE id = $2", - values: [handle, this.id], - }); - return result.handle; - } - - /** - * The email of the user - */ - public async email(): Promise { - if (!this.dataLoaded) { - await this.loadData(); - } - return this.$email; - } - - /** - * Sets the email of the user - * @param email - */ - public async setEmail(email: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE TABLE users SET email = $1 WHERE users.id = $2 RETURNING email", - values: [email, this.id], - }); - return result.email; - } - - /** - * The number of greenpoints of the user - */ - public async greenpoints(): Promise { - if (!this.dataLoaded) { - await this.loadData(); - } - return this.$greenpoints; - } - - /** - * Sets the greenpoints of a user. - * @param points - */ - public async setGreenpoints(points: number): Promise { - const result = await queryHelper.first({ - text: "UPDATE users SET greenpoints = $1 WHERE id = $2 RETURNING greenpoints", - values: [points, this.id], - }); - return result.greenpoints; - } - - /** - * The date the user joined the platform - */ - public async joinedAt(): Promise { - if (!this.dataLoaded) { - await this.loadData(); - } - return new Date(this.$joinedAt); - } - - /** - * Fetches the data for the user. - */ - private async loadData(): Promise { - let result: any; - if (this.row) { - result = this.row; - } else { - result = await queryHelper.first({ - text: "SELECT * FROM users WHERE user.id = $1", - values: [this.id], - }); - } - if (result) { - this.$name = result.name; - this.$handle = result.handle; - this.$email = result.email; - this.$greenpoints = result.greenpoints; - this.$joinedAt = result.joined_at; - this.dataLoaded = true; - } - } -} - -export class Post { - public readonly id: number; - private $upvotes: number; - private $downvotes: number; - private $createdAt: string; - private $content: string; - private $author: number; - private $type: string; - private dataLoaded: boolean = false; - - constructor(id: number, private row?: any) { - this.id = id; - } - - /** - * Returns the upvotes of a post. - */ - public async upvotes() { - if (!this.dataLoaded) { - await this.loadData(); - } - return this.$upvotes; - } - - /** - * Returns the downvotes of the post - */ - public async downvotes() { - if (!this.dataLoaded) { - await this.loadData(); - } - return this.$downvotes; - } - - /** - * Loads the data from the database if needed. - */ - private async loadData(): Promise { - let result: any; - if (this.row) { - result = this.row; - } else { - result = await queryHelper.first({ - text: "SELECT * FROM posts WHERE posts.id = $1", - values: [this.id], - }); - } - if (result) { - this.$author = result.author; - this.$content = result.content; - this.$downvotes = result.downvotes; - this.$upvotes = result.upvotes; - this.$createdAt = result.created_at; - this.$type = result.type; - this.dataLoaded = true; - } - } -} diff --git a/src/lib/QueryHelper.ts b/src/lib/QueryHelper.ts index 7586276..5970f70 100644 --- a/src/lib/QueryHelper.ts +++ b/src/lib/QueryHelper.ts @@ -68,9 +68,10 @@ export class QueryHelper { /** * Constructor. * @param pgPool - * @param tableCreationFile + * @param [tableCreationFile] + * @param [tableUpdateFile] */ - constructor(pgPool: Pool, private tableCreationFile?: string) { + constructor(pgPool: Pool, private tableCreationFile?: string, private tableUpdateFile?: string) { this.pool = pgPool; } @@ -85,6 +86,17 @@ export class QueryHelper { } } + /** + * Updates the definition of the tables if the table update file was passed in the constructor + */ + public async updateTableDefinitions() { + if (this.tableUpdateFile) { + logger.info("Updating table definitions..."); + const tableSql = await fsx.readFile(this.tableUpdateFile, "utf-8"); + await this.query({text: tableSql}); + } + } + /** * executes the sql query with values and returns all results. * @param query diff --git a/src/lib/dataaccess/DataObject.ts b/src/lib/dataaccess/DataObject.ts new file mode 100644 index 0000000..1890425 --- /dev/null +++ b/src/lib/dataaccess/DataObject.ts @@ -0,0 +1,20 @@ +/** + * abstact DataObject class + */ +export abstract class DataObject { + protected dataLoaded: boolean = false; + + constructor(public id: number, protected row?: any) { + } + + protected abstract loadData(): Promise; + + /** + * Loads data from the database if data has not been loaded + */ + protected loadDataIfNotExists() { + if (this.dataLoaded) { + this.loadData(); + } + } +} diff --git a/src/lib/dataaccess/Post.ts b/src/lib/dataaccess/Post.ts new file mode 100644 index 0000000..b4193c4 --- /dev/null +++ b/src/lib/dataaccess/Post.ts @@ -0,0 +1,103 @@ +import {DataObject} from "./DataObject"; +import {queryHelper} from "./index"; +import dataaccess from "./index"; +import {User} from "./User"; + +export class Post extends DataObject { + public readonly id: number; + private $upvotes: number; + private $downvotes: number; + private $createdAt: string; + private $content: string; + private $author: number; + private $type: string; + + /** + * Returns the upvotes of a post. + */ + public async upvotes(): Promise { + this.loadDataIfNotExists(); + return this.$upvotes; + } + + /** + * Returns the downvotes of the post + */ + public async downvotes(): Promise { + this.loadDataIfNotExists(); + return this.$downvotes; + } + + /** + * The content of the post (markdown) + */ + public async content(): Promise { + this.loadDataIfNotExists(); + return this.$content; + } + + /** + * The date the post was created at. + */ + public async createdAt(): Promise { + this.loadDataIfNotExists(); + return this.$createdAt; + } + + /** + * The autor of the post. + */ + public async author(): Promise { + this.loadDataIfNotExists(); + return new User(this.$author); + } + + /** + * Deletes the post. + */ + public async delete(): Promise { + const query = await queryHelper.first({ + text: "DELETE FROM posts WHERE id = $1", + values: [this.id], + }); + } + + /** + * The type of vote the user performed on the post. + */ + public async userVote(userId: number): Promise { + const result = await queryHelper.first({ + text: "SELECT vote_type FROM votes WHERE user_id = $1 AND item_id = $2", + values: [userId, this.id], + }); + if (result) { + return result.vote_type; + } else { + return null; + } + } + + /** + * Loads the data from the database if needed. + */ + protected async loadData(): Promise { + let result: any; + if (this.row) { + result = this.row; + } else { + result = await queryHelper.first({ + text: "SELECT * FROM posts WHERE posts.id = $1", + values: [this.id], + }); + } + if (result) { + this.$author = result.author; + this.$content = result.content; + this.$downvotes = result.downvotes; + this.$upvotes = result.upvotes; + this.$createdAt = result.created_at; + this.$type = result.type; + this.dataLoaded = true; + } + } +} diff --git a/src/lib/dataaccess/User.ts b/src/lib/dataaccess/User.ts new file mode 100644 index 0000000..bb680c4 --- /dev/null +++ b/src/lib/dataaccess/User.ts @@ -0,0 +1,137 @@ +import {DataObject} from "./DataObject"; +import {queryHelper} from "./index"; +import {Post} from "./Post"; + +export class User extends DataObject { + private $name: string; + private $handle: string; + private $email: string; + private $greenpoints: number; + private $joinedAt: string; + + /** + * The name of the user + */ + public async name(): Promise { + this.loadDataIfNotExists(); + return this.$name; + } + + /** + * Sets the username of the user + * @param name + */ + public async setName(name: string): Promise { + const result = await queryHelper.first({ + text: "UPDATE TABLE users SET name = $1 WHERE id = $2", + values: [name, this.id], + }); + return result.name; + } + + /** + * The unique handle of the user. + */ + public async handle(): Promise { + this.loadDataIfNotExists(); + return this.$handle; + } + + /** + * Updates the handle of the user + */ + public async setHandle(handle: string): Promise { + const result = await queryHelper.first({ + text: "UPDATE TABLE users SET handle = $1 WHERE id = $2", + values: [handle, this.id], + }); + return result.handle; + } + + /** + * The email of the user + */ + public async email(): Promise { + this.loadDataIfNotExists(); + return this.$email; + } + + /** + * Sets the email of the user + * @param email + */ + public async setEmail(email: string): Promise { + const result = await queryHelper.first({ + text: "UPDATE TABLE users SET email = $1 WHERE users.id = $2 RETURNING email", + values: [email, this.id], + }); + return result.email; + } + + /** + * The number of greenpoints of the user + */ + public async greenpoints(): Promise { + this.loadDataIfNotExists(); + return this.$greenpoints; + } + + /** + * Sets the greenpoints of a user. + * @param points + */ + public async setGreenpoints(points: number): Promise { + const result = await queryHelper.first({ + text: "UPDATE users SET greenpoints = $1 WHERE id = $2 RETURNING greenpoints", + values: [points, this.id], + }); + return result.greenpoints; + } + + /** + * The date the user joined the platform + */ + public async joinedAt(): Promise { + this.loadDataIfNotExists(); + return new Date(this.$joinedAt); + } + + /** + * Returns all posts for a user. + */ + public async posts(): Promise { + const result = await queryHelper.all({ + text: "SELECT * FROM posts WHERE author = $1", + values: [this.id], + }); + const posts = []; + + for (const row of result) { + posts.push(new Post(row.id, row)); + } + return posts; + } + + /** + * Fetches the data for the user. + */ + protected async loadData(): Promise { + let result: any; + if (this.row) { + result = this.row; + } else { + result = await queryHelper.first({ + text: "SELECT * FROM users WHERE user.id = $1", + values: [this.id], + }); + } + if (result) { + this.$name = result.name; + this.$handle = result.handle; + this.$email = result.email; + this.$greenpoints = result.greenpoints; + this.$joinedAt = result.joined_at; + this.dataLoaded = true; + } + } +} diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts new file mode 100644 index 0000000..1d610ef --- /dev/null +++ b/src/lib/dataaccess/index.ts @@ -0,0 +1,66 @@ +import {Pool} from "pg"; +import globals from "../globals"; +import {QueryHelper} from "../QueryHelper"; +import {User} from "./User"; + +const config = globals.config; +const tableCreationFile = __dirname + "/../../sql/create-tables.sql"; +const tableUpdateFile = __dirname + "/../../sql/update-tables.sql"; + +const dbClient: Pool = new Pool({ + database: config.database.database, + host: config.database.host, + password: config.database.password, + port: config.database.port, + user: config.database.user, +}); +export const queryHelper = new QueryHelper(dbClient, tableCreationFile, tableUpdateFile); + +namespace dataaccess { + /** + * Initializes everything that needs to be initialized asynchronous. + */ + export async function init() { + await queryHelper.updateTableDefinitions(); + await queryHelper.createTables(); + } + + /** + * Returns the user by id + * @param userId + */ + export function getUser(userId: number) { + return new User(userId); + } + + /** + * Returns the user by handle. + * @param userHandle + */ + export async function getUserByHandle(userHandle: string) { + const result = await this.queryHelper.first({ + text: "SELECT * FROM users WHERE users.handle = $1", + values: [userHandle], + }); + return new User(result.id, result); + } + + /** + * Enum representing the types of votes that can be performed on a post. + */ + export enum VoteType { + UPVOTE = "UPVOTE", + DOWNVOTE = "DOWNVOTE", + } + + /** + * Enum representing the types of request that can be created. + */ + export enum RequestType { + FRIENDREQUEST = "FRIENDREQUEST", + GROUPINVITE = "GROUPINVITE", + EVENTINVITE = "EVENTINVITE", + } +} + +export default dataaccess; diff --git a/src/public/graphql/schema.graphql b/src/public/graphql/schema.graphql index f5feb67..ff60fd3 100644 --- a/src/public/graphql/schema.graphql +++ b/src/public/graphql/schema.graphql @@ -1,100 +1,145 @@ type Query { - "returns the user object for a given user id" - getUser(userId: ID): User - "returns the post object for a post id" - getPost(postId: ID): Post - "returns the chat object for a chat id" - getChat(chatId: ID): ChatRoom - "returns the request object for a request id" - getRequest(requestId: ID): Request - "find a post by the posted date or content" - findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post] - "find a user by user name or handle" - findUser(first: Int, offset: Int, name: String!, handle: String!): [User] + "returns the user object for a given user id" + getUser(userId: ID): User + + "returns the post object for a post id" + getPost(postId: ID): Post + + "returns the chat object for a chat id" + getChat(chatId: ID): ChatRoom + + "returns the request object for a request id" + getRequest(requestId: ID): Request + + "find a post by the posted date or content" + findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post] + + "find a user by user name or handle" + findUser(first: Int, offset: Int, name: String!, handle: String!): [User] } type Mutation { - "Upvote/downvote a Post" - vote(postId: ID!, type: [VoteType!]!): Boolean - "Report the post" - report(postId: ID!): Boolean + "Upvote/downvote a Post" + vote(postId: ID!, type: [VoteType!]!): Boolean + + "Report the post" + report(postId: ID!): Boolean + + "send a request" + sendRequest(reciever: ID!, type: RequestType): Boolean + "lets you accept a request for a given request id" - acceptRequest(requestId: ID!): Boolean + acceptRequest(requestId: ID!): Boolean + + "lets you deny a request for a given request id" + denyRequest(requestId: ID!): Boolean + "send a message in a Chatroom" - sendMessage(chatId: ID!, content: String!): Boolean + sendMessage(chatId: ID!, content: String!): Boolean + + "create the post" + createPost(text: String, picture: String, tags: [String]): Boolean + + "delete the post for a given post id" + deletePost(postId: ID!): Boolean } "represents a single user account" type User { - "url for the Profile picture of the User" - profilePicture: String! - "name of the User" - name: String! - "unique identifier name from the User" - handle: String! - "Id of the User" - id: ID! - "the total number of posts the user posted" - numberOfPosts: Int - "returns a given number of posts of a user" - getAllPosts(first: Int=10, offset: Int): [Post] - "creation date of the user account" - joinedDate: String! - "returns chats the user pinned" - pinnedChats: [ChatRoom] - "returns all friends of the user" - friends: [User] - "all request for groupChats/friends/events" - requests: [Request] + "url for the Profile picture of the User" + profilePicture: String! + + "name of the User" + name: String! + + "unique identifier name from the User" + handle: String! + + "Id of the User" + id: ID! + + "the total number of posts the user posted" + numberOfPosts: Int + + "returns a given number of posts of a user" + getAllPosts(first: Int=10, offset: Int): [Post] + + "creation date of the user account" + joinedDate: String! + + "returns chats the user pinned" + pinnedChats: [ChatRoom] + + "returns all friends of the user" + friends: [User] + + "all request for groupChats/friends/events" + requests: [Request] } "represents a single user post" type Post { - "returns the path to the posts picture if it has one" - picture: String - "returns the text of the post" - text: String - "upvotes of the Post" - upvotes: Int! - "downvotes of the Post" - downvotes: Int! - "the user that is the author of the Post" - author: User! - "date the post was created" - creationDate: String! - "returns the type of vote the user performed on the post" - alreadyVoted: VoteType - "returns the tags of the post" - tags: [String] + "returns the path to the posts picture if it has one" + picture: String + + "returns the text of the post" + text: String + + "upvotes of the Post" + upvotes: Int! + + "downvotes of the Post" + downvotes: Int! + + "the user that is the author of the Post" + author: User! + + "date the post was created" + creationDate: String! + + "returns the type of vote the user performed on the post" + userVote: VoteType + + "returns the tags of the post" + tags: [String] } "represents a request of any type" type Request { - "id of the request" - id: ID! - "type of the request" - requestType: RequestType! + "id of the request" + id: ID! + + "Id of the user who sended the request" + sender: User! + + "Id of the user who received the request" + receiver: User! + + "type of the request" + requestType: RequestType! } "represents a chatroom" type ChatRoom { - "the members of the chatroom" - members: [User!] - "return a specfic range of messages posted in the chat" - getMessages(first: Int, offset: Int): [String] - "id of the chat" - id: ID! + "the members of the chatroom" + members: [User!] + + "return a specfic range of messages posted in the chat" + getMessages(first: Int, offset: Int): [String] + + "id of the chat" + id: ID! } "represents the type of vote performed on a post" enum VoteType { - UPVOTE - DOWNVOTE + UPVOTE + DOWNVOTE } "represents the type of request that the user has received" enum RequestType { - FRIENDREQUEST - GROUPINVITE - EVENTINVITE + FRIENDREQUEST + GROUPINVITE + EVENTINVITE } diff --git a/src/sql/create-tables.sql b/src/sql/create-tables.sql index 0d02b2f..7c1c97a 100644 --- a/src/sql/create-tables.sql +++ b/src/sql/create-tables.sql @@ -20,7 +20,8 @@ CREATE TABLE IF NOT EXISTS posts ( CREATE TABLE IF NOT EXISTS votes ( user_id SERIAL REFERENCES users (id) ON DELETE CASCADE, - item_id BIGSERIAL REFERENCES posts (id) ON DELETE CASCADE + item_id BIGSERIAL REFERENCES posts (id) ON DELETE CASCADE, + vote_type varchar(8) DEFAULT 'upvote' ); CREATE TABLE IF NOT EXISTS events ( diff --git a/src/sql/update-tables.sql b/src/sql/update-tables.sql new file mode 100644 index 0000000..52ce2f6 --- /dev/null +++ b/src/sql/update-tables.sql @@ -0,0 +1,3 @@ +ALTER TABLE IF EXISTS votes + ADD COLUMN IF NOT EXISTS vote_type varchar(8) DEFAULT 'upvote', + ALTER COLUMN vote_type SET DEFAULT 'upvote';