From 7a468feb8f4a9c2e2ad39d77d01697cd3c9d95c5 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Sun, 14 Apr 2024 14:21:10 +1000 Subject: [PATCH 01/18] Move initial migration to one file --- .../20240406023149_create_users.sql | 15 -- .../20240406023149_intial_schema.sql | 217 ++++++++++++++++++ .../20240406024211_create_organisations.sql | 20 -- .../20240406025537_create_campaigns.sql | 35 --- .../20240406031400_create_questions.sql | 30 --- .../20240406031915_create_applications.sql | 113 --------- 6 files changed, 217 insertions(+), 213 deletions(-) delete mode 100644 backend/migrations/20240406023149_create_users.sql create mode 100644 backend/migrations/20240406023149_intial_schema.sql delete mode 100644 backend/migrations/20240406024211_create_organisations.sql delete mode 100644 backend/migrations/20240406025537_create_campaigns.sql delete mode 100644 backend/migrations/20240406031400_create_questions.sql delete mode 100644 backend/migrations/20240406031915_create_applications.sql diff --git a/backend/migrations/20240406023149_create_users.sql b/backend/migrations/20240406023149_create_users.sql deleted file mode 100644 index 0bb4cd8b..00000000 --- a/backend/migrations/20240406023149_create_users.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TYPE user_role AS ENUM ('User', 'SuperUser'); - -CREATE TABLE users ( - id BIGINT PRIMARY KEY, - email TEXT NOT NULL UNIQUE, - zid TEXT, - name TEXT NOT NULL, - degree_name TEXT, - degree_starting_year INTEGER, - role user_role NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -); - -CREATE UNIQUE INDEX IDX_users_email_lower on users ((lower(email))); diff --git a/backend/migrations/20240406023149_intial_schema.sql b/backend/migrations/20240406023149_intial_schema.sql new file mode 100644 index 00000000..098e65bc --- /dev/null +++ b/backend/migrations/20240406023149_intial_schema.sql @@ -0,0 +1,217 @@ +CREATE TYPE user_role AS ENUM ('User', 'SuperUser'); + +CREATE TABLE users ( + id BIGINT PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + zid TEXT, + name TEXT NOT NULL, + degree_name TEXT, + degree_starting_year INTEGER, + role user_role NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX IDX_users_email_lower on users ((lower(email))); + +CREATE TABLE organisations ( + id BIGINT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + logo TEXT, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE organisation_admins ( + id SERIAL PRIMARY KEY, + organisation_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + CONSTRAINT FK_organisation_admins_organisation + FOREIGN KEY(organisation_id) + REFERENCES organisations(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_organisation_admins_organisation on organisation_admins (organisation_id); + +CREATE TABLE campaigns ( + id BIGINT PRIMARY KEY, + organisation_id BIGINT NOT NULL, + name TEXT NOT NULL, + cover_image TEXT, + description TEXT, + starts_at TIMESTAMPTZ NOT NULL, + ends_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_campaigns_organisations + FOREIGN KEY(organisation_id) + REFERENCES organisations(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE campaign_roles ( + id SERIAL PRIMARY KEY, + campaign_id BIGINT NOT NULL, + name TEXT NOT NULL, + description TEXT, + min_available INTEGER, + max_available INTEGER, + finalised BOOLEAN, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_campaign_roles_campaign + FOREIGN KEY(campaign_id) + REFERENCES campaigns(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_campaign_roles_campaign on campaign_roles (campaign_id); + +CREATE TYPE question_type AS ENUM ('ShortAnswer', 'MultiChoice', 'MultiSelect', 'DropDown'); + +CREATE TABLE questions ( + id BIGINT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + required BOOLEAN, + question_type question_type NOT NULL, + campaign_id BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_questions_campaigns + FOREIGN KEY(campaign_id) + REFERENCES campaigns(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE multi_option_question_options ( + id SERIAL PRIMARY KEY, + text TEXT NOT NULL, + question_id INTEGER NOT NULL, + CONSTRAINT FK_multi_option_question_options_questions + FOREIGN KEY(question_id) + REFERENCES questions(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_multi_option_question_options_questions on multi_option_question_options (question_id); + +CREATE TYPE application_status AS ENUM ('Pending', 'Rejected', 'Successful'); + +CREATE TABLE applications ( + id BIGINT PRIMARY KEY, + campaign_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + status application_status NOT NULL, + private_status application_status NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_applications_campaigns + FOREIGN KEY(campaign_id) + REFERENCES campaigns(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_applications_users + FOREIGN KEY(user_id) + REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE application_roles ( + id SERIAL PRIMARY KEY, + application_id INTEGER NOT NULL, + campaign_role_id INTEGER NOT NULL, + CONSTRAINT FK_application_roles_applications + FOREIGN KEY(application_id) + REFERENCES applications(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_application_roles_campaign_roles + FOREIGN KEY(campaign_role_id) + REFERENCES campaign_roles(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_application_roles_applications on application_roles (application_id); +CREATE INDEX IDX_application_roles_campaign_roles on application_roles (campaign_role_id); + +CREATE TABLE answers ( + id SERIAL PRIMARY KEY, + application_id BIGINT NOT NULL, + question_id BIGINT NOT NULL, + CONSTRAINT FK_answers_applications + FOREIGN KEY(application_id) + REFERENCES applications(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_answers_questions + FOREIGN KEY(question_id) + REFERENCES questions(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_answers_applications on answers (application_id); +CREATE INDEX IDX_answers_questions on answers (question_id); + +CREATE TABLE short_answer_answers ( + id SERIAL PRIMARY KEY, + text TEXT NOT NULL, + answer_id INTEGER NOT NULL, + CONSTRAINT FK_short_answer_answers_answers + FOREIGN KEY(answer_id) + REFERENCES answers(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_short_answer_answers_answers on short_answer_answers (answer_id); + +CREATE TABLE multi_option_answer_options ( + id SERIAL PRIMARY KEY, + option_id BIGINT NOT NULL, + answer_id INTEGER NOT NULL, + CONSTRAINT FK_multi_option_answer_options_question_options + FOREIGN KEY(option_id) + REFERENCES multi_option_question_options(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_multi_option_answer_options_answers + FOREIGN KEY(answer_id) + REFERENCES answers(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_multi_option_answer_options_question_options on multi_option_answer_options (option_id); +CREATE INDEX IDX_multi_option_answer_options_answers on multi_option_answer_options (answer_id); + +CREATE TABLE application_ratings ( + id SERIAL PRIMARY KEY, + application_id BIGINT NOT NULL, + rater_id BIGINT NOT NULL, + rating INTEGER NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_application_ratings_applications + FOREIGN KEY(application_id) + REFERENCES applications(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_application_ratings_users + FOREIGN KEY(rater_id) + REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_application_ratings_applications on application_ratings (application_id); +CREATE INDEX IDX_application_ratings_users on application_ratings (rater_id); diff --git a/backend/migrations/20240406024211_create_organisations.sql b/backend/migrations/20240406024211_create_organisations.sql deleted file mode 100644 index 88dda366..00000000 --- a/backend/migrations/20240406024211_create_organisations.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE TABLE organisations ( - id BIGINT PRIMARY KEY, - name TEXT NOT NULL UNIQUE, - logo TEXT, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE organisation_admins ( - id SERIAL PRIMARY KEY, - organisation_id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - CONSTRAINT FK_organisation_admins_organisation - FOREIGN KEY(organisation_id) - REFERENCES organisations(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_organisation_admins_organisation on organisation_admins (organisation_id); diff --git a/backend/migrations/20240406025537_create_campaigns.sql b/backend/migrations/20240406025537_create_campaigns.sql deleted file mode 100644 index 46bca9df..00000000 --- a/backend/migrations/20240406025537_create_campaigns.sql +++ /dev/null @@ -1,35 +0,0 @@ -CREATE TABLE campaigns ( - id BIGINT PRIMARY KEY, - organisation_id BIGINT NOT NULL, - name TEXT NOT NULL, - cover_image TEXT, - description TEXT, - starts_at TIMESTAMPTZ NOT NULL, - ends_at TIMESTAMPTZ NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT FK_campaigns_organisations - FOREIGN KEY(organisation_id) - REFERENCES organisations(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE TABLE campaign_roles ( - id SERIAL PRIMARY KEY, - campaign_id BIGINT NOT NULL, - name TEXT NOT NULL, - description TEXT, - min_available INTEGER, - max_available INTEGER, - finalised BOOLEAN, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT FK_campaign_roles_campaign - FOREIGN KEY(campaign_id) - REFERENCES campaigns(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_campaign_roles_campaign on campaign_roles (campaign_id); diff --git a/backend/migrations/20240406031400_create_questions.sql b/backend/migrations/20240406031400_create_questions.sql deleted file mode 100644 index fa170813..00000000 --- a/backend/migrations/20240406031400_create_questions.sql +++ /dev/null @@ -1,30 +0,0 @@ -CREATE TYPE question_type AS ENUM ('ShortAnswer', 'MultiChoice', 'MultiSelect', 'DropDown'); - -CREATE TABLE questions ( - id BIGINT PRIMARY KEY, - title TEXT NOT NULL, - description TEXT, - required BOOLEAN, - question_type question_type NOT NULL, - campaign_id BIGINT NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT FK_questions_campaigns - FOREIGN KEY(campaign_id) - REFERENCES campaigns(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE TABLE multi_option_question_options ( - id SERIAL PRIMARY KEY, - text TEXT NOT NULL, - question_id INTEGER NOT NULL, - CONSTRAINT FK_multi_option_question_options_questions - FOREIGN KEY(question_id) - REFERENCES questions(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_multi_option_question_options_questions on multi_option_question_options (question_id); diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql deleted file mode 100644 index 91d9edcf..00000000 --- a/backend/migrations/20240406031915_create_applications.sql +++ /dev/null @@ -1,113 +0,0 @@ -CREATE TYPE application_status AS ENUM ('Pending', 'Rejected', 'Successful'); - -CREATE TABLE applications ( - id BIGINT PRIMARY KEY, - campaign_id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - status application_status NOT NULL, - private_status application_status NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT FK_applications_campaigns - FOREIGN KEY(campaign_id) - REFERENCES campaigns(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - CONSTRAINT FK_applications_users - FOREIGN KEY(user_id) - REFERENCES users(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE TABLE application_roles ( - id SERIAL PRIMARY KEY, - application_id INTEGER NOT NULL, - campaign_role_id INTEGER NOT NULL, - CONSTRAINT FK_application_roles_applications - FOREIGN KEY(application_id) - REFERENCES applications(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - CONSTRAINT FK_application_roles_campaign_roles - FOREIGN KEY(campaign_role_id) - REFERENCES campaign_roles(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_application_roles_applications on application_roles (application_id); -CREATE INDEX IDX_application_roles_campaign_roles on application_roles (campaign_role_id); - -CREATE TABLE answers ( - id SERIAL PRIMARY KEY, - application_id BIGINT NOT NULL, - question_id BIGINT NOT NULL, - CONSTRAINT FK_answers_applications - FOREIGN KEY(application_id) - REFERENCES applications(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - CONSTRAINT FK_answers_questions - FOREIGN KEY(question_id) - REFERENCES questions(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_answers_applications on answers (application_id); -CREATE INDEX IDX_answers_questions on answers (question_id); - -CREATE TABLE short_answer_answers ( - id SERIAL PRIMARY KEY, - text TEXT NOT NULL, - answer_id INTEGER NOT NULL, - CONSTRAINT FK_short_answer_answers_answers - FOREIGN KEY(answer_id) - REFERENCES answers(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_short_answer_answers_answers on short_answer_answers (answer_id); - -CREATE TABLE multi_option_answer_options ( - id SERIAL PRIMARY KEY, - option_id BIGINT NOT NULL, - answer_id INTEGER NOT NULL, - CONSTRAINT FK_multi_option_answer_options_question_options - FOREIGN KEY(option_id) - REFERENCES multi_option_question_options(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - CONSTRAINT FK_multi_option_answer_options_answers - FOREIGN KEY(answer_id) - REFERENCES answers(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_multi_option_answer_options_question_options on multi_option_answer_options (option_id); -CREATE INDEX IDX_multi_option_answer_options_answers on multi_option_answer_options (answer_id); - -CREATE TABLE application_ratings ( - id SERIAL PRIMARY KEY, - application_id BIGINT NOT NULL, - rater_id BIGINT NOT NULL, - rating INTEGER NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT FK_application_ratings_applications - FOREIGN KEY(application_id) - REFERENCES applications(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - CONSTRAINT FK_application_ratings_users - FOREIGN KEY(rater_id) - REFERENCES users(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_application_ratings_applications on application_ratings (application_id); -CREATE INDEX IDX_application_ratings_users on application_ratings (rater_id); From 1ea8b95db4f3dd759a02712b336f48a3ce9deb05 Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Sun, 14 Apr 2024 14:26:05 +1000 Subject: [PATCH 02/18] Update initial schema NOT NULL columns --- .../20240406023149_intial_schema.sql | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/backend/migrations/20240406023149_intial_schema.sql b/backend/migrations/20240406023149_intial_schema.sql index 098e65bc..6de04b5d 100644 --- a/backend/migrations/20240406023149_intial_schema.sql +++ b/backend/migrations/20240406023149_intial_schema.sql @@ -8,8 +8,8 @@ CREATE TABLE users ( degree_name TEXT, degree_starting_year INTEGER, role user_role NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL ); CREATE UNIQUE INDEX IDX_users_email_lower on users ((lower(email))); @@ -18,8 +18,8 @@ CREATE TABLE organisations ( id BIGINT PRIMARY KEY, name TEXT NOT NULL UNIQUE, logo TEXT, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL ); CREATE TABLE organisation_admins ( @@ -43,8 +43,8 @@ CREATE TABLE campaigns ( description TEXT, starts_at TIMESTAMPTZ NOT NULL, ends_at TIMESTAMPTZ NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_campaigns_organisations FOREIGN KEY(organisation_id) REFERENCES organisations(id) @@ -60,8 +60,8 @@ CREATE TABLE campaign_roles ( min_available INTEGER, max_available INTEGER, finalised BOOLEAN, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_campaign_roles_campaign FOREIGN KEY(campaign_id) REFERENCES campaigns(id) @@ -77,11 +77,11 @@ CREATE TABLE questions ( id BIGINT PRIMARY KEY, title TEXT NOT NULL, description TEXT, - required BOOLEAN, + required BOOLEAN NOT NULL, question_type question_type NOT NULL, campaign_id BIGINT NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_questions_campaigns FOREIGN KEY(campaign_id) REFERENCES campaigns(id) @@ -110,8 +110,8 @@ CREATE TABLE applications ( user_id BIGINT NOT NULL, status application_status NOT NULL, private_status application_status NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_applications_campaigns FOREIGN KEY(campaign_id) REFERENCES campaigns(id) @@ -199,8 +199,8 @@ CREATE TABLE application_ratings ( application_id BIGINT NOT NULL, rater_id BIGINT NOT NULL, rating INTEGER NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_application_ratings_applications FOREIGN KEY(application_id) REFERENCES applications(id) From 4273cef1d4a7f461586f48bf558844578592299a Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Sun, 14 Apr 2024 15:04:09 +1000 Subject: [PATCH 03/18] Fix question_id ref column being int not bigint --- backend/migrations/20240406023149_intial_schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/migrations/20240406023149_intial_schema.sql b/backend/migrations/20240406023149_intial_schema.sql index 6de04b5d..d8a39747 100644 --- a/backend/migrations/20240406023149_intial_schema.sql +++ b/backend/migrations/20240406023149_intial_schema.sql @@ -92,7 +92,7 @@ CREATE TABLE questions ( CREATE TABLE multi_option_question_options ( id SERIAL PRIMARY KEY, text TEXT NOT NULL, - question_id INTEGER NOT NULL, + question_id BIGINT NOT NULL, CONSTRAINT FK_multi_option_question_options_questions FOREIGN KEY(question_id) REFERENCES questions(id) From 991f5ae6a1a9c952937663f0b09f78a60f1c765a Mon Sep 17 00:00:00 2001 From: Kavika <50282464+KavikaPalletenne@users.noreply.github.com> Date: Sun, 14 Apr 2024 15:55:25 +1000 Subject: [PATCH 04/18] Question framework and impl for MultiOption and ShortAnswer --- backend/server/Cargo.toml | 2 +- backend/server/src/models/mod.rs | 1 + backend/server/src/models/question.rs | 131 ++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 backend/server/src/models/question.rs diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index fd764748..bf341390 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" tokio = { version = "1.34", features = ["macros", "rt-multi-thread"] } axum = { version = "0.7", features = ["macros"] } axum-extra = { version = "0.9", features = ["typed-header"] } -sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "time", "uuid"] } +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "time", "uuid", "chrono"] } # Important secondary crates anyhow = "1.0" diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index b992e0f9..abfcd5ad 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -1,4 +1,5 @@ pub mod app; pub mod auth; pub mod error; +pub mod question; pub mod user; diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs new file mode 100644 index 00000000..326e6476 --- /dev/null +++ b/backend/server/src/models/question.rs @@ -0,0 +1,131 @@ +use crate::models::error::ChaosError; +use anyhow::{bail, Result}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::{Pool, Postgres, QueryBuilder, Row}; + +#[derive(Deserialize, Serialize)] +pub struct Question { + id: i64, + title: String, + description: Option, + required: bool, + + #[serde(flatten)] + question_type: QuestionType, + + campaign_id: i64, + created_at: DateTime, + updated_at: DateTime, +} + +/// An enum that represents all the data types of question data that CHAOS can handle. +/// This stores all the data for each question type. +/// +/// \ +/// Some question types are stored in memory and JSON using the same struct, and only differ +/// in their implementation when inserting to the database and in their restrictions +/// (e.g. max 1 answer allowed in multi-choice vs. many in multi-select) +/// +/// \ +/// With the chosen serde representation and the use of `#[serde(flatten)]`, the JSON for a +/// `Question` will look like this: +/// ```json +/// { +/// "title": "What is your favourite language?", +/// "required": true, +/// "question_type": "MultiChoice", +/// "data": [ +/// "Rust", +/// "Java", +/// "TypeScript" +/// ] +/// } +/// ``` +#[derive(Deserialize, Serialize)] +#[serde(tag = "question_type", content = "data")] +pub enum QuestionType { + ShortAnswer, + MultiChoice(MultiOptionData), + MultiSelect(MultiOptionData), + DropDown(MultiOptionData), +} + +pub trait QuestionData { + async fn insert_into_db(self, question_id: i64, pool: &Pool) -> Result; + async fn get_from_db(self, question_id: i64, pool: &Pool) -> Result; +} + +#[derive(Deserialize, Serialize)] +pub struct MultiOptionData { + options: Vec, +} + +impl QuestionData for MultiOptionData { + async fn insert_into_db(self, question_id: i64, pool: &Pool) -> Result { + let mut query_builder = + QueryBuilder::new("INSERT INTO multi_option_question_options (text, question_id) "); + + query_builder.push_values(self.options, |mut b, option| { + b.push_bind(option).push_bind(question_id); + }); + + query_builder.push("RETURNING id"); + + let query = query_builder.build(); + let result = query.fetch_one(pool).await?; + + Ok(result.get("id")) + } + + async fn get_from_db(self, question_id: i64, pool: &Pool) -> Result { + let result = sqlx::query!( + "SELECT text FROM multi_option_question_options + WHERE question_id = $1", + question_id + ) + .fetch_all(pool) + .await?; + + let options = result.into_iter().map(|r| r.text).collect(); + + Ok(QuestionType::MultiChoice(MultiOptionData { options })) + } +} + +/// Each of these structs represent a row in the `multi_option_question_options` +/// table. For a `MultiChoice` question like "What is your favourite programming +/// language?", there would be rows for "Rust", "Java" and "TypeScript". +#[derive(Deserialize, Serialize)] +pub struct MultiOptionQuestionOption { + id: i32, + text: String, + question_id: i64, +} + +impl QuestionType { + pub async fn validate(self) -> Result<()> { + match self { + Self::ShortAnswer => Ok(()), + Self::MultiChoice(data) + | Self::MultiSelect(data) + | Self::DropDown(data) => { + if data.options.len() > 0 { + return Ok(()); + }; + + bail!("Invalid number of options.") + } + } + } + pub async fn insert_into_db(self, question_id: i64, pool: &Pool) -> Result { + match self { + Self::ShortAnswer => Ok(question_id), + Self::MultiChoice(data) + | Self::MultiSelect(data) + | Self::DropDown(data) => { + data.insert_into_db(question_id, pool).await + } + } + } +} From 44a08cfcf1e22d8ff3adfed75dcd613aa5940143 Mon Sep 17 00:00:00 2001 From: skye_blair Date: Fri, 28 Jun 2024 00:55:35 +1000 Subject: [PATCH 05/18] answer framework for short answer and multichoice --- backend/server/src/models/answer.rs | 92 +++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 backend/server/src/models/answer.rs diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs new file mode 100644 index 00000000..6cfc543f --- /dev/null +++ b/backend/server/src/models/answer.rs @@ -0,0 +1,92 @@ +use crate::models::error::ChaosError; +use anyhow::{bail, Result}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::{Pool, Postgres, QueryBuilder, Row}; + +#[derive(Deserialize, Serialize)] +pub struct Answer { + id: i64, + application_id: i64, + question_id: i64, + created_at: DateTime, + updated_at: DateTime, +} + +#[derive(Deserialize, Serialize)] +pub enum AnswerType { + ShortAnswerData(ShortAnswerData), + MultiOptionAnswerData(MultiOptionAnswerData), +} + +#[derive(Deserialize, Serialize)] +pub struct ShortAnswerData { + text: String, +} + +#[derive(Deserialize, Serialize)] +pub struct MultiOptionAnswerData { + options: Vec, +} + +pub trait AnswerData { + async fn insert_into_db(self, answer_id: i64, pool: &Pool) -> Result; + async fn get_from_db(answer_id: i64, pool: &Pool) -> Result + where + Self: Sized; +} + +impl AnswerData for ShortAnswerData { + async fn insert_into_db(self, answer_id: i64, pool: &Pool) -> Result { + let result = sqlx::query!( + "INSERT INTO short_answer_answers (text, answer_id) VALUES ($1, $2) RETURNING id", + self.text, + answer_id + ) + .fetch_one(pool) + .await?; + + Ok(result.get("id")) + } + + async fn get_from_db(answer_id: i64, pool: &Pool) -> Result { + let result = sqlx::query!( + "SELECT text FROM short_answer_answers WHERE answer_id = $1", + answer_id + ) + .fetch_one(pool) + .await?; + + Ok(ShortAnswerData { text: result.get("text") }) + } +} + +impl AnswerData for MultiOptionAnswerData { + async fn insert_into_db(self, answer_id: i64, pool: &Pool) -> Result { + let mut query_builder = sqlx::QueryBuilder::new("INSERT INTO multi_option_answer_options (option_id, answer_id) "); + + query_builder.push_values(&self.options, |mut b, option_id| { + b.push_bind(option_id).push_bind(answer_id); + }); + + query_builder.push(" RETURNING id"); + + let query = query_builder.build(); + let result = query.fetch_one(pool).await?; + + Ok(result.get("id")) + } + + async fn get_from_db(answer_id: i64, pool: &Pool) -> Result { + let result = sqlx::query!( + "SELECT option_id FROM multi_option_answer_options WHERE answer_id = $1", + answer_id + ) + .fetch_all(pool) + .await?; + + let options = result.into_iter().map(|r| r.get("option_id")).collect(); + + Ok(MultiOptionAnswerData { options }) + } +} From 2a6f28f18511239256e75df8d5a3c779bc785315 Mon Sep 17 00:00:00 2001 From: Kavika Date: Mon, 26 Aug 2024 19:05:23 +0530 Subject: [PATCH 06/18] update `Question` documentation to fit `serde` representation --- backend/server/src/models/question.rs | 39 ++++++++++++++------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs index 326e6476..6529a46a 100644 --- a/backend/server/src/models/question.rs +++ b/backend/server/src/models/question.rs @@ -4,9 +4,28 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{Pool, Postgres, QueryBuilder, Row}; +/// The `Question` type that will be sent in API responses. +/// +/// +/// With the chosen `serde` representation and the use of `#[serde(flatten)]`, the JSON for a +/// `Question` will look like this: +/// ```json +/// { +/// "id": "7233828375289139200", +/// "title": "What is your favourite language?", +/// "required": true, +/// "question_type": "MultiChoice", +/// "data": { +/// "options": ["Rust", "Java", "TypeScript"] +/// }, +/// "campaign_id": "7233828393345617920", +/// "created_at": "2024-06-28T16:29:04.644008111Z", +/// "updated_at": "2024-06-30T12:14:12.458390190Z" +/// } +/// ``` #[derive(Deserialize, Serialize)] pub struct Question { - id: i64, + id: String, title: String, description: Option, required: bool, @@ -14,7 +33,7 @@ pub struct Question { #[serde(flatten)] question_type: QuestionType, - campaign_id: i64, + campaign_id: String, created_at: DateTime, updated_at: DateTime, } @@ -26,22 +45,6 @@ pub struct Question { /// Some question types are stored in memory and JSON using the same struct, and only differ /// in their implementation when inserting to the database and in their restrictions /// (e.g. max 1 answer allowed in multi-choice vs. many in multi-select) -/// -/// \ -/// With the chosen serde representation and the use of `#[serde(flatten)]`, the JSON for a -/// `Question` will look like this: -/// ```json -/// { -/// "title": "What is your favourite language?", -/// "required": true, -/// "question_type": "MultiChoice", -/// "data": [ -/// "Rust", -/// "Java", -/// "TypeScript" -/// ] -/// } -/// ``` #[derive(Deserialize, Serialize)] #[serde(tag = "question_type", content = "data")] pub enum QuestionType { From 075687f7e9f1a6e55d1c7b306a755dd3198ac37e Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 10:26:54 +1100 Subject: [PATCH 07/18] fix duplicates in question framework --- .../20240406023149_intial_schema.sql | 5 +- backend/server/src/models/question.rs | 74 +++++++------------ 2 files changed, 29 insertions(+), 50 deletions(-) diff --git a/backend/migrations/20240406023149_intial_schema.sql b/backend/migrations/20240406023149_intial_schema.sql index d8a39747..ce74e244 100644 --- a/backend/migrations/20240406023149_intial_schema.sql +++ b/backend/migrations/20240406023149_intial_schema.sql @@ -77,6 +77,7 @@ CREATE TABLE questions ( id BIGINT PRIMARY KEY, title TEXT NOT NULL, description TEXT, + common BOOLEAN NOT NULL, required BOOLEAN NOT NULL, question_type question_type NOT NULL, campaign_id BIGINT NOT NULL, @@ -93,11 +94,13 @@ CREATE TABLE multi_option_question_options ( id SERIAL PRIMARY KEY, text TEXT NOT NULL, question_id BIGINT NOT NULL, + rank INTEGER NOT NULL, CONSTRAINT FK_multi_option_question_options_questions FOREIGN KEY(question_id) REFERENCES questions(id) ON DELETE CASCADE - ON UPDATE CASCADE + ON UPDATE CASCADE, + UNIQUE (text, question_id) ); CREATE INDEX IDX_multi_option_question_options_questions on multi_option_question_options (question_id); diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs index 6529a46a..db1856f9 100644 --- a/backend/server/src/models/question.rs +++ b/backend/server/src/models/question.rs @@ -11,7 +11,7 @@ use sqlx::{Pool, Postgres, QueryBuilder, Row}; /// `Question` will look like this: /// ```json /// { -/// "id": "7233828375289139200", +/// "id": 7233828375289139200, /// "title": "What is your favourite language?", /// "required": true, /// "question_type": "MultiChoice", @@ -23,17 +23,17 @@ use sqlx::{Pool, Postgres, QueryBuilder, Row}; /// "updated_at": "2024-06-30T12:14:12.458390190Z" /// } /// ``` -#[derive(Deserialize, Serialize)] +#[derive(Serialize)] pub struct Question { - id: String, + id: i64, title: String, description: Option, + common: bool, // Common question are shown at the start required: bool, #[serde(flatten)] - question_type: QuestionType, + question_data: QuestionData, - campaign_id: String, created_at: DateTime, updated_at: DateTime, } @@ -47,53 +47,16 @@ pub struct Question { /// (e.g. max 1 answer allowed in multi-choice vs. many in multi-select) #[derive(Deserialize, Serialize)] #[serde(tag = "question_type", content = "data")] -pub enum QuestionType { +pub enum QuestionData { ShortAnswer, MultiChoice(MultiOptionData), MultiSelect(MultiOptionData), DropDown(MultiOptionData), } -pub trait QuestionData { - async fn insert_into_db(self, question_id: i64, pool: &Pool) -> Result; - async fn get_from_db(self, question_id: i64, pool: &Pool) -> Result; -} - #[derive(Deserialize, Serialize)] pub struct MultiOptionData { - options: Vec, -} - -impl QuestionData for MultiOptionData { - async fn insert_into_db(self, question_id: i64, pool: &Pool) -> Result { - let mut query_builder = - QueryBuilder::new("INSERT INTO multi_option_question_options (text, question_id) "); - - query_builder.push_values(self.options, |mut b, option| { - b.push_bind(option).push_bind(question_id); - }); - - query_builder.push("RETURNING id"); - - let query = query_builder.build(); - let result = query.fetch_one(pool).await?; - - Ok(result.get("id")) - } - - async fn get_from_db(self, question_id: i64, pool: &Pool) -> Result { - let result = sqlx::query!( - "SELECT text FROM multi_option_question_options - WHERE question_id = $1", - question_id - ) - .fetch_all(pool) - .await?; - - let options = result.into_iter().map(|r| r.text).collect(); - - Ok(QuestionType::MultiChoice(MultiOptionData { options })) - } + options: Vec, } /// Each of these structs represent a row in the `multi_option_question_options` @@ -102,11 +65,11 @@ impl QuestionData for MultiOptionData { #[derive(Deserialize, Serialize)] pub struct MultiOptionQuestionOption { id: i32, + rank: i32, text: String, - question_id: i64, } -impl QuestionType { +impl QuestionData { pub async fn validate(self) -> Result<()> { match self { Self::ShortAnswer => Ok(()), @@ -121,13 +84,26 @@ impl QuestionType { } } } - pub async fn insert_into_db(self, question_id: i64, pool: &Pool) -> Result { + + pub async fn insert_into_db(self, question_id: i64, pool: &Pool) -> Result<()> { match self { - Self::ShortAnswer => Ok(question_id), + Self::ShortAnswer => Ok(()), Self::MultiChoice(data) | Self::MultiSelect(data) | Self::DropDown(data) => { - data.insert_into_db(question_id, pool).await + let mut query_builder = + QueryBuilder::new("INSERT INTO multi_option_question_options (text, question_id, rank) "); + + let mut rank = 1; + query_builder.push_values(self.options, |mut b, option| { + b.push_bind(option).push_bind(question_id).push_bind(rank); + rank += 1; + }); + + let query = query_builder.build(); + let result = query.execute(pool).await?; + + Ok(()) } } } From 2fb33064d21d5f0a78d5d2272f3874d29578770a Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 10:49:09 +1100 Subject: [PATCH 08/18] remove redundancy in answer framework --- backend/server/src/models/answer.rs | 141 ++++++++++++++-------------- 1 file changed, 72 insertions(+), 69 deletions(-) diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs index 6cfc543f..a2d56a9b 100644 --- a/backend/server/src/models/answer.rs +++ b/backend/server/src/models/answer.rs @@ -4,89 +4,92 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{Pool, Postgres, QueryBuilder, Row}; +/// The `Answer` type that will be sent in API responses. +/// +/// +/// With the chosen `serde` representation and the use of `#[serde(flatten)]`, the JSON for a +/// `Answer` will look like this: +/// ```json +/// { +/// "id": 7233828375289773948, +/// "application_id": 7233828375289125398, +/// "question_id": 7233828375289139200, +/// "answer_type": "MultiChoice", +/// "data": 7233828393325384908, +/// "created_at": "2024-06-28T16:29:04.644008111Z", +/// "updated_at": "2024-06-30T12:14:12.458390190Z" +/// } +/// ``` #[derive(Deserialize, Serialize)] pub struct Answer { id: i64, application_id: i64, question_id: i64, - created_at: DateTime, - updated_at: DateTime, -} -#[derive(Deserialize, Serialize)] -pub enum AnswerType { - ShortAnswerData(ShortAnswerData), - MultiOptionAnswerData(MultiOptionAnswerData), -} + #[serde(flatten)] + answer_data: AnswerData, -#[derive(Deserialize, Serialize)] -pub struct ShortAnswerData { - text: String, + created_at: DateTime, + updated_at: DateTime, } #[derive(Deserialize, Serialize)] -pub struct MultiOptionAnswerData { - options: Vec, -} - -pub trait AnswerData { - async fn insert_into_db(self, answer_id: i64, pool: &Pool) -> Result; - async fn get_from_db(answer_id: i64, pool: &Pool) -> Result - where - Self: Sized; -} - -impl AnswerData for ShortAnswerData { - async fn insert_into_db(self, answer_id: i64, pool: &Pool) -> Result { - let result = sqlx::query!( - "INSERT INTO short_answer_answers (text, answer_id) VALUES ($1, $2) RETURNING id", - self.text, - answer_id - ) - .fetch_one(pool) - .await?; - - Ok(result.get("id")) - } - - async fn get_from_db(answer_id: i64, pool: &Pool) -> Result { - let result = sqlx::query!( - "SELECT text FROM short_answer_answers WHERE answer_id = $1", - answer_id - ) - .fetch_one(pool) - .await?; - - Ok(ShortAnswerData { text: result.get("text") }) - } +#[serde( tag = "answer_type", content = "data")] +pub enum AnswerData { + ShortAnswer(String), + MultiChoice(i64), + MultiSelect(Vec), + DropDown(i64), } -impl AnswerData for MultiOptionAnswerData { - async fn insert_into_db(self, answer_id: i64, pool: &Pool) -> Result { - let mut query_builder = sqlx::QueryBuilder::new("INSERT INTO multi_option_answer_options (option_id, answer_id) "); +impl AnswerData { + pub async fn validate(self) -> Result<()> { + match self { + Self::ShortAnswer(text) => if text.len() <= 0 {bail!("Empty answer")}, + Self::MultiSelect(data) => if data.len() <= 0 {bail!("Empty answer")}, + _ => {}, + } - query_builder.push_values(&self.options, |mut b, option_id| { - b.push_bind(option_id).push_bind(answer_id); - }); - - query_builder.push(" RETURNING id"); - - let query = query_builder.build(); - let result = query.fetch_one(pool).await?; - - Ok(result.get("id")) + Ok(()) } - async fn get_from_db(answer_id: i64, pool: &Pool) -> Result { - let result = sqlx::query!( - "SELECT option_id FROM multi_option_answer_options WHERE answer_id = $1", - answer_id - ) - .fetch_all(pool) - .await?; - - let options = result.into_iter().map(|r| r.get("option_id")).collect(); - - Ok(MultiOptionAnswerData { options }) + pub async fn insert_into_db(self, answer_id: i64, pool: &Pool) -> Result<()> { + match self { + Self::ShortAnswer => { + let result = sqlx::query!( + "INSERT INTO short_answer_answers (text, answer_id) VALUES ($1, $2)", + self.text, + answer_id + ) + .execute(pool) + .await?; + + Ok(()) + }, + Self::MultiChoice(option_id) + | Self::DropDown(option_id) => { + let result = sqlx::query!( + "INSERT INTO multi_option_answer_options (option_id, answer_id) VALUES ($1, $2)", + option_id, + answer_id + ) + .execute(pool) + .await?; + + Ok(()) + }, + Self::MultiSelect(option_ids) => { + let mut query_builder = sqlx::QueryBuilder::new("INSERT INTO multi_option_answer_options (option_id, answer_id)"); + + query_builder.push_values(option_ids, |mut b, option_id| { + b.push_bind(option_id).push_bind(answer_id); + }); + + let query = query_builder.build(); + let result = query.execute(pool).await?; + + Ok(()) + } + } } } From 09f3bedc63d6a35702417c7bcbd6d57782e6492c Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 10:49:54 +1100 Subject: [PATCH 09/18] make question option id unique snowflake --- backend/server/src/models/question.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs index db1856f9..db6fca5e 100644 --- a/backend/server/src/models/question.rs +++ b/backend/server/src/models/question.rs @@ -3,6 +3,7 @@ use anyhow::{bail, Result}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{Pool, Postgres, QueryBuilder, Row}; +use snowflake::SnowflakeIdGenerator; /// The `Question` type that will be sent in API responses. /// @@ -18,7 +19,6 @@ use sqlx::{Pool, Postgres, QueryBuilder, Row}; /// "data": { /// "options": ["Rust", "Java", "TypeScript"] /// }, -/// "campaign_id": "7233828393345617920", /// "created_at": "2024-06-28T16:29:04.644008111Z", /// "updated_at": "2024-06-30T12:14:12.458390190Z" /// } @@ -85,18 +85,19 @@ impl QuestionData { } } - pub async fn insert_into_db(self, question_id: i64, pool: &Pool) -> Result<()> { + pub async fn insert_into_db(self, question_id: i64, pool: &Pool, mut snowflake_generator: SnowflakeIdGenerator) -> Result<()> { match self { Self::ShortAnswer => Ok(()), Self::MultiChoice(data) | Self::MultiSelect(data) | Self::DropDown(data) => { let mut query_builder = - QueryBuilder::new("INSERT INTO multi_option_question_options (text, question_id, rank) "); + QueryBuilder::new("INSERT INTO multi_option_question_options (id, text, question_id, rank)"); let mut rank = 1; query_builder.push_values(self.options, |mut b, option| { - b.push_bind(option).push_bind(question_id).push_bind(rank); + let id = snowflake_generator.real_time_generate(); + b.push_bind(id).push_bind(option).push_bind(question_id).push_bind(rank); rank += 1; }); From 48aed71759e46c35e0a16d23559673a53aee7a37 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 11:01:56 +1100 Subject: [PATCH 10/18] re-separate schema migrations --- .../20240406023149_create_users.sql | 15 ++ .../20240406023149_intial_schema.sql | 220 ------------------ .../20240406024211_create_organisations.sql | 20 ++ .../20240406025537_create_campaigns.sql | 35 +++ .../20240406031400_create_questions.sql | 31 +++ .../20240406031915_create_applications.sql | 113 +++++++++ 6 files changed, 214 insertions(+), 220 deletions(-) create mode 100644 backend/migrations/20240406023149_create_users.sql delete mode 100644 backend/migrations/20240406023149_intial_schema.sql create mode 100644 backend/migrations/20240406024211_create_organisations.sql create mode 100644 backend/migrations/20240406025537_create_campaigns.sql create mode 100644 backend/migrations/20240406031400_create_questions.sql create mode 100644 backend/migrations/20240406031915_create_applications.sql diff --git a/backend/migrations/20240406023149_create_users.sql b/backend/migrations/20240406023149_create_users.sql new file mode 100644 index 00000000..f4d87e0f --- /dev/null +++ b/backend/migrations/20240406023149_create_users.sql @@ -0,0 +1,15 @@ +CREATE TYPE user_role AS ENUM ('User', 'SuperUser'); + +CREATE TABLE users ( + id BIGINT PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + zid TEXT, + name TEXT NOT NULL, + degree_name TEXT, + degree_starting_year INTEGER, + role user_role NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX IDX_users_email_lower on users ((lower(email))); \ No newline at end of file diff --git a/backend/migrations/20240406023149_intial_schema.sql b/backend/migrations/20240406023149_intial_schema.sql deleted file mode 100644 index ce74e244..00000000 --- a/backend/migrations/20240406023149_intial_schema.sql +++ /dev/null @@ -1,220 +0,0 @@ -CREATE TYPE user_role AS ENUM ('User', 'SuperUser'); - -CREATE TABLE users ( - id BIGINT PRIMARY KEY, - email TEXT NOT NULL UNIQUE, - zid TEXT, - name TEXT NOT NULL, - degree_name TEXT, - degree_starting_year INTEGER, - role user_role NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL -); - -CREATE UNIQUE INDEX IDX_users_email_lower on users ((lower(email))); - -CREATE TABLE organisations ( - id BIGINT PRIMARY KEY, - name TEXT NOT NULL UNIQUE, - logo TEXT, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL -); - -CREATE TABLE organisation_admins ( - id SERIAL PRIMARY KEY, - organisation_id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - CONSTRAINT FK_organisation_admins_organisation - FOREIGN KEY(organisation_id) - REFERENCES organisations(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_organisation_admins_organisation on organisation_admins (organisation_id); - -CREATE TABLE campaigns ( - id BIGINT PRIMARY KEY, - organisation_id BIGINT NOT NULL, - name TEXT NOT NULL, - cover_image TEXT, - description TEXT, - starts_at TIMESTAMPTZ NOT NULL, - ends_at TIMESTAMPTZ NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - CONSTRAINT FK_campaigns_organisations - FOREIGN KEY(organisation_id) - REFERENCES organisations(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE TABLE campaign_roles ( - id SERIAL PRIMARY KEY, - campaign_id BIGINT NOT NULL, - name TEXT NOT NULL, - description TEXT, - min_available INTEGER, - max_available INTEGER, - finalised BOOLEAN, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - CONSTRAINT FK_campaign_roles_campaign - FOREIGN KEY(campaign_id) - REFERENCES campaigns(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_campaign_roles_campaign on campaign_roles (campaign_id); - -CREATE TYPE question_type AS ENUM ('ShortAnswer', 'MultiChoice', 'MultiSelect', 'DropDown'); - -CREATE TABLE questions ( - id BIGINT PRIMARY KEY, - title TEXT NOT NULL, - description TEXT, - common BOOLEAN NOT NULL, - required BOOLEAN NOT NULL, - question_type question_type NOT NULL, - campaign_id BIGINT NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - CONSTRAINT FK_questions_campaigns - FOREIGN KEY(campaign_id) - REFERENCES campaigns(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE TABLE multi_option_question_options ( - id SERIAL PRIMARY KEY, - text TEXT NOT NULL, - question_id BIGINT NOT NULL, - rank INTEGER NOT NULL, - CONSTRAINT FK_multi_option_question_options_questions - FOREIGN KEY(question_id) - REFERENCES questions(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - UNIQUE (text, question_id) -); - -CREATE INDEX IDX_multi_option_question_options_questions on multi_option_question_options (question_id); - -CREATE TYPE application_status AS ENUM ('Pending', 'Rejected', 'Successful'); - -CREATE TABLE applications ( - id BIGINT PRIMARY KEY, - campaign_id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - status application_status NOT NULL, - private_status application_status NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - CONSTRAINT FK_applications_campaigns - FOREIGN KEY(campaign_id) - REFERENCES campaigns(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - CONSTRAINT FK_applications_users - FOREIGN KEY(user_id) - REFERENCES users(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE TABLE application_roles ( - id SERIAL PRIMARY KEY, - application_id INTEGER NOT NULL, - campaign_role_id INTEGER NOT NULL, - CONSTRAINT FK_application_roles_applications - FOREIGN KEY(application_id) - REFERENCES applications(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - CONSTRAINT FK_application_roles_campaign_roles - FOREIGN KEY(campaign_role_id) - REFERENCES campaign_roles(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_application_roles_applications on application_roles (application_id); -CREATE INDEX IDX_application_roles_campaign_roles on application_roles (campaign_role_id); - -CREATE TABLE answers ( - id SERIAL PRIMARY KEY, - application_id BIGINT NOT NULL, - question_id BIGINT NOT NULL, - CONSTRAINT FK_answers_applications - FOREIGN KEY(application_id) - REFERENCES applications(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - CONSTRAINT FK_answers_questions - FOREIGN KEY(question_id) - REFERENCES questions(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_answers_applications on answers (application_id); -CREATE INDEX IDX_answers_questions on answers (question_id); - -CREATE TABLE short_answer_answers ( - id SERIAL PRIMARY KEY, - text TEXT NOT NULL, - answer_id INTEGER NOT NULL, - CONSTRAINT FK_short_answer_answers_answers - FOREIGN KEY(answer_id) - REFERENCES answers(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_short_answer_answers_answers on short_answer_answers (answer_id); - -CREATE TABLE multi_option_answer_options ( - id SERIAL PRIMARY KEY, - option_id BIGINT NOT NULL, - answer_id INTEGER NOT NULL, - CONSTRAINT FK_multi_option_answer_options_question_options - FOREIGN KEY(option_id) - REFERENCES multi_option_question_options(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - CONSTRAINT FK_multi_option_answer_options_answers - FOREIGN KEY(answer_id) - REFERENCES answers(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_multi_option_answer_options_question_options on multi_option_answer_options (option_id); -CREATE INDEX IDX_multi_option_answer_options_answers on multi_option_answer_options (answer_id); - -CREATE TABLE application_ratings ( - id SERIAL PRIMARY KEY, - application_id BIGINT NOT NULL, - rater_id BIGINT NOT NULL, - rating INTEGER NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, - CONSTRAINT FK_application_ratings_applications - FOREIGN KEY(application_id) - REFERENCES applications(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - CONSTRAINT FK_application_ratings_users - FOREIGN KEY(rater_id) - REFERENCES users(id) - ON DELETE CASCADE - ON UPDATE CASCADE -); - -CREATE INDEX IDX_application_ratings_applications on application_ratings (application_id); -CREATE INDEX IDX_application_ratings_users on application_ratings (rater_id); diff --git a/backend/migrations/20240406024211_create_organisations.sql b/backend/migrations/20240406024211_create_organisations.sql new file mode 100644 index 00000000..b956d4d7 --- /dev/null +++ b/backend/migrations/20240406024211_create_organisations.sql @@ -0,0 +1,20 @@ +CREATE TABLE organisations ( + id BIGINT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + logo TEXT, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE organisation_admins ( + id SERIAL PRIMARY KEY, + organisation_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + CONSTRAINT FK_organisation_admins_organisation + FOREIGN KEY(organisation_id) + REFERENCES organisations(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_organisation_admins_organisation on organisation_admins (organisation_id); \ No newline at end of file diff --git a/backend/migrations/20240406025537_create_campaigns.sql b/backend/migrations/20240406025537_create_campaigns.sql new file mode 100644 index 00000000..44553fb1 --- /dev/null +++ b/backend/migrations/20240406025537_create_campaigns.sql @@ -0,0 +1,35 @@ +CREATE TABLE campaigns ( + id BIGINT PRIMARY KEY, + organisation_id BIGINT NOT NULL, + name TEXT NOT NULL, + cover_image TEXT, + description TEXT, + starts_at TIMESTAMPTZ NOT NULL, + ends_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_campaigns_organisations + FOREIGN KEY(organisation_id) + REFERENCES organisations(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE campaign_roles ( + id SERIAL PRIMARY KEY, + campaign_id BIGINT NOT NULL, + name TEXT NOT NULL, + description TEXT, + min_available INTEGER, + max_available INTEGER, + finalised BOOLEAN, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_campaign_roles_campaign + FOREIGN KEY(campaign_id) + REFERENCES campaigns(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_campaign_roles_campaign on campaign_roles (campaign_id); \ No newline at end of file diff --git a/backend/migrations/20240406031400_create_questions.sql b/backend/migrations/20240406031400_create_questions.sql new file mode 100644 index 00000000..bd74f309 --- /dev/null +++ b/backend/migrations/20240406031400_create_questions.sql @@ -0,0 +1,31 @@ +CREATE TYPE question_type AS ENUM ('ShortAnswer', 'MultiChoice', 'MultiSelect', 'DropDown'); + +CREATE TABLE questions ( + id BIGINT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + common BOOLEAN NOT NULL, + required BOOLEAN, + question_type question_type NOT NULL, + campaign_id BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_questions_campaigns + FOREIGN KEY(campaign_id) + REFERENCES campaigns(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE multi_option_question_options ( + id BIGINT PRIMARY KEY, + text TEXT NOT NULL, + question_id BIGINT NOT NULL, + CONSTRAINT FK_multi_option_question_options_questions + FOREIGN KEY(question_id) + REFERENCES questions(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_multi_option_question_options_questions on multi_option_question_options (question_id); \ No newline at end of file diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql new file mode 100644 index 00000000..adfdf4bc --- /dev/null +++ b/backend/migrations/20240406031915_create_applications.sql @@ -0,0 +1,113 @@ +CREATE TYPE application_status AS ENUM ('Pending', 'Rejected', 'Successful'); + +CREATE TABLE applications ( + id BIGINT PRIMARY KEY, + campaign_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + status application_status NOT NULL, + private_status application_status NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_applications_campaigns + FOREIGN KEY(campaign_id) + REFERENCES campaigns(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_applications_users + FOREIGN KEY(user_id) + REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE TABLE application_roles ( + id SERIAL PRIMARY KEY, + application_id INTEGER NOT NULL, + campaign_role_id INTEGER NOT NULL, + CONSTRAINT FK_application_roles_applications + FOREIGN KEY(application_id) + REFERENCES applications(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_application_roles_campaign_roles + FOREIGN KEY(campaign_role_id) + REFERENCES campaign_roles(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_application_roles_applications on application_roles (application_id); +CREATE INDEX IDX_application_roles_campaign_roles on application_roles (campaign_role_id); + +CREATE TABLE answers ( + id BIGINT PRIMARY KEY, + application_id BIGINT NOT NULL, + question_id BIGINT NOT NULL, + CONSTRAINT FK_answers_applications + FOREIGN KEY(application_id) + REFERENCES applications(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_answers_questions + FOREIGN KEY(question_id) + REFERENCES questions(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_answers_applications on answers (application_id); +CREATE INDEX IDX_answers_questions on answers (question_id); + +CREATE TABLE short_answer_answers ( + id SERIAL PRIMARY KEY, + text TEXT NOT NULL, + answer_id BIGINT NOT NULL, + CONSTRAINT FK_short_answer_answers_answers + FOREIGN KEY(answer_id) + REFERENCES answers(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_short_answer_answers_answers on short_answer_answers (answer_id); + +CREATE TABLE multi_option_answer_options ( + id SERIAL PRIMARY KEY, + option_id BIGINT NOT NULL, + answer_id BIGINT NOT NULL, + CONSTRAINT FK_multi_option_answer_options_question_options + FOREIGN KEY(option_id) + REFERENCES multi_option_question_options(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_multi_option_answer_options_answers + FOREIGN KEY(answer_id) + REFERENCES answers(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_multi_option_answer_options_question_options on multi_option_answer_options (option_id); +CREATE INDEX IDX_multi_option_answer_options_answers on multi_option_answer_options (answer_id); + +CREATE TABLE application_ratings ( + id SERIAL PRIMARY KEY, + application_id BIGINT NOT NULL, + rater_id BIGINT NOT NULL, + rating INTEGER NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_application_ratings_applications + FOREIGN KEY(application_id) + REFERENCES applications(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_application_ratings_users + FOREIGN KEY(rater_id) + REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX IDX_application_ratings_applications on application_ratings (application_id); +CREATE INDEX IDX_application_ratings_users on application_ratings (rater_id); \ No newline at end of file From daa1d10927c5af77db8becabfaef344c51decb99 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 11:04:32 +1100 Subject: [PATCH 11/18] set timestamps fields to `NOT NULL` --- backend/migrations/20240406023149_create_users.sql | 4 ++-- .../migrations/20240406024211_create_organisations.sql | 4 ++-- backend/migrations/20240406025537_create_campaigns.sql | 8 ++++---- backend/migrations/20240406031400_create_questions.sql | 4 ++-- backend/migrations/20240406031915_create_applications.sql | 8 ++++---- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/backend/migrations/20240406023149_create_users.sql b/backend/migrations/20240406023149_create_users.sql index f4d87e0f..580daf60 100644 --- a/backend/migrations/20240406023149_create_users.sql +++ b/backend/migrations/20240406023149_create_users.sql @@ -8,8 +8,8 @@ CREATE TABLE users ( degree_name TEXT, degree_starting_year INTEGER, role user_role NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IDX_users_email_lower on users ((lower(email))); \ No newline at end of file diff --git a/backend/migrations/20240406024211_create_organisations.sql b/backend/migrations/20240406024211_create_organisations.sql index b956d4d7..40aebedb 100644 --- a/backend/migrations/20240406024211_create_organisations.sql +++ b/backend/migrations/20240406024211_create_organisations.sql @@ -2,8 +2,8 @@ CREATE TABLE organisations ( id BIGINT PRIMARY KEY, name TEXT NOT NULL UNIQUE, logo TEXT, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE organisation_admins ( diff --git a/backend/migrations/20240406025537_create_campaigns.sql b/backend/migrations/20240406025537_create_campaigns.sql index 44553fb1..07db1d0a 100644 --- a/backend/migrations/20240406025537_create_campaigns.sql +++ b/backend/migrations/20240406025537_create_campaigns.sql @@ -6,8 +6,8 @@ CREATE TABLE campaigns ( description TEXT, starts_at TIMESTAMPTZ NOT NULL, ends_at TIMESTAMPTZ NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT FK_campaigns_organisations FOREIGN KEY(organisation_id) REFERENCES organisations(id) @@ -23,8 +23,8 @@ CREATE TABLE campaign_roles ( min_available INTEGER, max_available INTEGER, finalised BOOLEAN, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT FK_campaign_roles_campaign FOREIGN KEY(campaign_id) REFERENCES campaigns(id) diff --git a/backend/migrations/20240406031400_create_questions.sql b/backend/migrations/20240406031400_create_questions.sql index bd74f309..825fff35 100644 --- a/backend/migrations/20240406031400_create_questions.sql +++ b/backend/migrations/20240406031400_create_questions.sql @@ -8,8 +8,8 @@ CREATE TABLE questions ( required BOOLEAN, question_type question_type NOT NULL, campaign_id BIGINT NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT FK_questions_campaigns FOREIGN KEY(campaign_id) REFERENCES campaigns(id) diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql index adfdf4bc..b97728fc 100644 --- a/backend/migrations/20240406031915_create_applications.sql +++ b/backend/migrations/20240406031915_create_applications.sql @@ -6,8 +6,8 @@ CREATE TABLE applications ( user_id BIGINT NOT NULL, status application_status NOT NULL, private_status application_status NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT FK_applications_campaigns FOREIGN KEY(campaign_id) REFERENCES campaigns(id) @@ -95,8 +95,8 @@ CREATE TABLE application_ratings ( application_id BIGINT NOT NULL, rater_id BIGINT NOT NULL, rating INTEGER NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT FK_application_ratings_applications FOREIGN KEY(application_id) REFERENCES applications(id) From 30134858ba1a0510a01d7d4af805d069885099ef Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 11:05:57 +1100 Subject: [PATCH 12/18] add answer to models module file --- backend/server/src/models/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/server/src/models/mod.rs b/backend/server/src/models/mod.rs index abfcd5ad..56b06c32 100644 --- a/backend/server/src/models/mod.rs +++ b/backend/server/src/models/mod.rs @@ -1,3 +1,4 @@ +pub mod answer; pub mod app; pub mod auth; pub mod error; From 06efb2cf444d0cfef2bb08007e370dd102a902f8 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 11:17:23 +1100 Subject: [PATCH 13/18] fix enum errors --- backend/server/src/models/answer.rs | 4 ++-- backend/server/src/models/question.rs | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs index a2d56a9b..e4fb1d9a 100644 --- a/backend/server/src/models/answer.rs +++ b/backend/server/src/models/answer.rs @@ -55,10 +55,10 @@ impl AnswerData { pub async fn insert_into_db(self, answer_id: i64, pool: &Pool) -> Result<()> { match self { - Self::ShortAnswer => { + Self::ShortAnswer(text) => { let result = sqlx::query!( "INSERT INTO short_answer_answers (text, answer_id) VALUES ($1, $2)", - self.text, + text, answer_id ) .execute(pool) diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs index db6fca5e..1c6771f3 100644 --- a/backend/server/src/models/question.rs +++ b/backend/server/src/models/question.rs @@ -94,11 +94,9 @@ impl QuestionData { let mut query_builder = QueryBuilder::new("INSERT INTO multi_option_question_options (id, text, question_id, rank)"); - let mut rank = 1; - query_builder.push_values(self.options, |mut b, option| { + query_builder.push_values(data.options, |mut b, option| { let id = snowflake_generator.real_time_generate(); - b.push_bind(id).push_bind(option).push_bind(question_id).push_bind(rank); - rank += 1; + b.push_bind(id).push_bind(option.text).push_bind(question_id).push_bind(option.rank); }); let query = query_builder.build(); From 848d476af90a56d203d85f50c6cb53641adbe6f0 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 11:22:21 +1100 Subject: [PATCH 14/18] fix always true/false case in answer length --- backend/server/src/models/answer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs index e4fb1d9a..4fa71579 100644 --- a/backend/server/src/models/answer.rs +++ b/backend/server/src/models/answer.rs @@ -45,8 +45,8 @@ pub enum AnswerData { impl AnswerData { pub async fn validate(self) -> Result<()> { match self { - Self::ShortAnswer(text) => if text.len() <= 0 {bail!("Empty answer")}, - Self::MultiSelect(data) => if data.len() <= 0 {bail!("Empty answer")}, + Self::ShortAnswer(text) => if text.len() == 0 { bail!("Empty answer") }, + Self::MultiSelect(data) => if data.len() == 0 { bail!("Empty answer") }, _ => {}, } From f49168481dc7f970516d6c23d75c3df8fdd2a7a4 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 11:55:28 +1100 Subject: [PATCH 15/18] add `Ranking` question type --- .../20240406031400_create_questions.sql | 2 +- .../20240406031915_create_applications.sql | 17 +++++++++++++++++ backend/server/src/models/answer.rs | 15 +++++++++++++++ backend/server/src/models/question.rs | 7 +++++-- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/backend/migrations/20240406031400_create_questions.sql b/backend/migrations/20240406031400_create_questions.sql index 825fff35..f9706333 100644 --- a/backend/migrations/20240406031400_create_questions.sql +++ b/backend/migrations/20240406031400_create_questions.sql @@ -1,4 +1,4 @@ -CREATE TYPE question_type AS ENUM ('ShortAnswer', 'MultiChoice', 'MultiSelect', 'DropDown'); +CREATE TYPE question_type AS ENUM ('ShortAnswer', 'MultiChoice', 'MultiSelect', 'DropDown', 'Ranking'); CREATE TABLE questions ( id BIGINT PRIMARY KEY, diff --git a/backend/migrations/20240406031915_create_applications.sql b/backend/migrations/20240406031915_create_applications.sql index 89d690b0..7d1053b3 100644 --- a/backend/migrations/20240406031915_create_applications.sql +++ b/backend/migrations/20240406031915_create_applications.sql @@ -87,6 +87,23 @@ CREATE TABLE multi_option_answer_options ( ON UPDATE CASCADE ); +CREATE TABLE ranking_answer_rankings ( + id BIGSERIAL PRIMARY KEY, + option_id BIGINT NOT NULL, + rank INTEGER NOT NULL, + answer_id BIGINT NOT NULL, + CONSTRAINT FK_ranking_answer_rankings_question_options + FOREIGN KEY(option_id) + REFERENCES multi_option_question_options(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT FK_ranking_answer_rankings_answers + FOREIGN KEY(answer_id) + REFERENCES answers(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + CREATE INDEX IDX_multi_option_answer_options_question_options on multi_option_answer_options (option_id); CREATE INDEX IDX_multi_option_answer_options_answers on multi_option_answer_options (answer_id); diff --git a/backend/server/src/models/answer.rs b/backend/server/src/models/answer.rs index 4fa71579..f79bf9d6 100644 --- a/backend/server/src/models/answer.rs +++ b/backend/server/src/models/answer.rs @@ -40,6 +40,7 @@ pub enum AnswerData { MultiChoice(i64), MultiSelect(Vec), DropDown(i64), + Ranking(Vec) } impl AnswerData { @@ -88,6 +89,20 @@ impl AnswerData { let query = query_builder.build(); let result = query.execute(pool).await?; + Ok(()) + }, + Self::Ranking(option_ids) => { + let mut query_builder = sqlx::QueryBuilder::new("INSERT INTO ranking_answer_rankings (option_id, rank, answer_id)"); + + let mut rank = 1; + query_builder.push_values(option_ids, |mut b, option_id| { + b.push_bind(option_id).push_bind(rank).push_bind(answer_id); + rank += 1; + }); + + let query = query_builder.build(); + let result = query.execute(pool).await?; + Ok(()) } } diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs index 1c6771f3..93870a90 100644 --- a/backend/server/src/models/question.rs +++ b/backend/server/src/models/question.rs @@ -52,6 +52,7 @@ pub enum QuestionData { MultiChoice(MultiOptionData), MultiSelect(MultiOptionData), DropDown(MultiOptionData), + Ranking(MultiOptionData), } #[derive(Deserialize, Serialize)] @@ -75,7 +76,8 @@ impl QuestionData { Self::ShortAnswer => Ok(()), Self::MultiChoice(data) | Self::MultiSelect(data) - | Self::DropDown(data) => { + | Self::DropDown(data) + | Self::Ranking(data) => { if data.options.len() > 0 { return Ok(()); }; @@ -90,7 +92,8 @@ impl QuestionData { Self::ShortAnswer => Ok(()), Self::MultiChoice(data) | Self::MultiSelect(data) - | Self::DropDown(data) => { + | Self::DropDown(data) + | Self::Ranking(data) => { let mut query_builder = QueryBuilder::new("INSERT INTO multi_option_question_options (id, text, question_id, rank)"); From 1bfb005119b91bd63e605e039adef2fe72096925 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 20:29:55 +1100 Subject: [PATCH 16/18] rename multi option data "rank" to "order" --- backend/migrations/20240406031400_create_questions.sql | 1 + backend/server/src/models/question.rs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/migrations/20240406031400_create_questions.sql b/backend/migrations/20240406031400_create_questions.sql index f9706333..bb7442bb 100644 --- a/backend/migrations/20240406031400_create_questions.sql +++ b/backend/migrations/20240406031400_create_questions.sql @@ -21,6 +21,7 @@ CREATE TABLE multi_option_question_options ( id BIGINT PRIMARY KEY, text TEXT NOT NULL, question_id BIGINT NOT NULL, + order INTEGER NOT NULL, CONSTRAINT FK_multi_option_question_options_questions FOREIGN KEY(question_id) REFERENCES questions(id) diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs index 93870a90..f1a5a3cc 100644 --- a/backend/server/src/models/question.rs +++ b/backend/server/src/models/question.rs @@ -66,7 +66,7 @@ pub struct MultiOptionData { #[derive(Deserialize, Serialize)] pub struct MultiOptionQuestionOption { id: i32, - rank: i32, + order: i32, text: String, } @@ -95,11 +95,11 @@ impl QuestionData { | Self::DropDown(data) | Self::Ranking(data) => { let mut query_builder = - QueryBuilder::new("INSERT INTO multi_option_question_options (id, text, question_id, rank)"); + QueryBuilder::new("INSERT INTO multi_option_question_options (id, text, question_id, order)"); query_builder.push_values(data.options, |mut b, option| { let id = snowflake_generator.real_time_generate(); - b.push_bind(id).push_bind(option.text).push_bind(question_id).push_bind(option.rank); + b.push_bind(id).push_bind(option.text).push_bind(question_id).push_bind(option.order); }); let query = query_builder.build(); From 2d1eef7631de26077f4bd7c580acef1485f987d3 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 20:34:53 +1100 Subject: [PATCH 17/18] update question json example --- backend/server/src/models/question.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs index f1a5a3cc..2bd3e137 100644 --- a/backend/server/src/models/question.rs +++ b/backend/server/src/models/question.rs @@ -17,7 +17,23 @@ use snowflake::SnowflakeIdGenerator; /// "required": true, /// "question_type": "MultiChoice", /// "data": { -/// "options": ["Rust", "Java", "TypeScript"] +/// "options": [ +/// { +/// "id": 7233828375387640938, +/// "order": 1, +/// "text": "Rust" +/// }, +/// { +/// "id": 7233828375387640954, +/// "order": 2, +/// "text": "Java" +/// }, +/// { +/// "id": 7233828375387640374, +/// "order": 3, +/// "text": "TypeScript" +/// } +/// ] /// }, /// "created_at": "2024-06-28T16:29:04.644008111Z", /// "updated_at": "2024-06-30T12:14:12.458390190Z" From 0e83c9944f8595af34c04fc7f98db6824c83acf0 Mon Sep 17 00:00:00 2001 From: Kavika Date: Tue, 5 Nov 2024 20:37:19 +1100 Subject: [PATCH 18/18] fix use of postgres keyword `order` --- .../migrations/20240406031400_create_questions.sql | 2 +- backend/server/src/models/question.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/migrations/20240406031400_create_questions.sql b/backend/migrations/20240406031400_create_questions.sql index bb7442bb..24f01d97 100644 --- a/backend/migrations/20240406031400_create_questions.sql +++ b/backend/migrations/20240406031400_create_questions.sql @@ -21,7 +21,7 @@ CREATE TABLE multi_option_question_options ( id BIGINT PRIMARY KEY, text TEXT NOT NULL, question_id BIGINT NOT NULL, - order INTEGER NOT NULL, + display_order INTEGER NOT NULL, CONSTRAINT FK_multi_option_question_options_questions FOREIGN KEY(question_id) REFERENCES questions(id) diff --git a/backend/server/src/models/question.rs b/backend/server/src/models/question.rs index 2bd3e137..307a7aae 100644 --- a/backend/server/src/models/question.rs +++ b/backend/server/src/models/question.rs @@ -20,17 +20,17 @@ use snowflake::SnowflakeIdGenerator; /// "options": [ /// { /// "id": 7233828375387640938, -/// "order": 1, +/// "display_order": 1, /// "text": "Rust" /// }, /// { /// "id": 7233828375387640954, -/// "order": 2, +/// "display_order": 2, /// "text": "Java" /// }, /// { /// "id": 7233828375387640374, -/// "order": 3, +/// "display_order": 3, /// "text": "TypeScript" /// } /// ] @@ -82,7 +82,7 @@ pub struct MultiOptionData { #[derive(Deserialize, Serialize)] pub struct MultiOptionQuestionOption { id: i32, - order: i32, + display_order: i32, text: String, } @@ -111,11 +111,11 @@ impl QuestionData { | Self::DropDown(data) | Self::Ranking(data) => { let mut query_builder = - QueryBuilder::new("INSERT INTO multi_option_question_options (id, text, question_id, order)"); + QueryBuilder::new("INSERT INTO multi_option_question_options (id, text, question_id, display_order)"); query_builder.push_values(data.options, |mut b, option| { let id = snowflake_generator.real_time_generate(); - b.push_bind(id).push_bind(option.text).push_bind(question_id).push_bind(option.order); + b.push_bind(id).push_bind(option.text).push_bind(question_id).push_bind(option.display_order); }); let query = query_builder.build();