From 95e22172b94421d1297f84452a358893012d283f Mon Sep 17 00:00:00 2001 From: Martin Benedikt Busch <43137759+MartinBenediktBusch@users.noreply.github.com> Date: Wed, 10 Apr 2024 18:23:38 +0200 Subject: [PATCH 01/21] add question to group category table (#308) --- migrations/0015_sad_thunderbolt_ross.sql | 19 + migrations/meta/0015_snapshot.json | 1508 ++++++++++++++++++++++ migrations/meta/_journal.json | 7 + src/db/forumQuestions.ts | 2 + src/db/groupCategories.ts | 2 + src/db/index.ts | 1 + src/db/questionsToGroupCategories.ts | 30 + 7 files changed, 1569 insertions(+) create mode 100644 migrations/0015_sad_thunderbolt_ross.sql create mode 100644 migrations/meta/0015_snapshot.json create mode 100644 src/db/questionsToGroupCategories.ts diff --git a/migrations/0015_sad_thunderbolt_ross.sql b/migrations/0015_sad_thunderbolt_ross.sql new file mode 100644 index 00000000..b3a99c91 --- /dev/null +++ b/migrations/0015_sad_thunderbolt_ross.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS "questions_to_group_categories" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "question_id" uuid NOT NULL, + "group_category_id" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "questions_to_group_categories" ADD CONSTRAINT "questions_to_group_categories_question_id_forum_questions_id_fk" FOREIGN KEY ("question_id") REFERENCES "forum_questions"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "questions_to_group_categories" ADD CONSTRAINT "questions_to_group_categories_group_category_id_group_categories_id_fk" FOREIGN KEY ("group_category_id") REFERENCES "group_categories"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/migrations/meta/0015_snapshot.json b/migrations/meta/0015_snapshot.json new file mode 100644 index 00000000..9217bb97 --- /dev/null +++ b/migrations/meta/0015_snapshot.json @@ -0,0 +1,1508 @@ +{ + "id": "d8506015-c303-43f5-8f18-20673b1869c2", + "prevId": "2bbf7b5d-a98a-4170-8e2c-df6dcbab7133", + "version": "5", + "dialect": "pg", + "tables": { + "comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_option_id": { + "name": "question_option_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comments_question_option_id_question_options_id_fk": { + "name": "comments_question_option_id_question_options_id_fk", + "tableFrom": "comments", + "tableTo": "question_options", + "columnsFrom": [ + "question_option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "cycles": { + "name": "cycles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'UPCOMING'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cycles_event_id_events_id_fk": { + "name": "cycles_event_id_events_id_fk", + "tableFrom": "cycles", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "registration_description": { + "name": "registration_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_display_rank": { + "name": "event_display_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "federated_credentials": { + "name": "federated_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federated_credentials_user_id_users_id_fk": { + "name": "federated_credentials_user_id_users_id_fk", + "tableFrom": "federated_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_subject_idx": { + "name": "provider_subject_idx", + "nullsNotDistinct": false, + "columns": [ + "provider", + "subject" + ] + } + } + }, + "forum_questions": { + "name": "forum_questions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_title": { + "name": "question_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "question_sub_title": { + "name": "question_sub_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "forum_questions_cycle_id_cycles_id_fk": { + "name": "forum_questions_cycle_id_cycles_id_fk", + "tableFrom": "forum_questions", + "tableTo": "cycles", + "columnsFrom": [ + "cycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "group_categories": { + "name": "group_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "group_categories_event_id_events_id_fk": { + "name": "group_categories_event_id_events_id_fk", + "tableFrom": "group_categories", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "groups": { + "name": "groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "groups_group_category_id_group_categories_id_fk": { + "name": "groups_group_category_id_group_categories_id_fk", + "tableFrom": "groups", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram": { + "name": "telegram", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_telegram_unique": { + "name": "users_telegram_unique", + "nullsNotDistinct": false, + "columns": [ + "telegram" + ] + } + } + }, + "registrations": { + "name": "registrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'DRAFT'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registrations_user_id_users_id_fk": { + "name": "registrations_user_id_users_id_fk", + "tableFrom": "registrations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registrations_event_id_events_id_fk": { + "name": "registrations_event_id_events_id_fk", + "tableFrom": "registrations", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_field_options": { + "name": "registration_field_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_field_id": { + "name": "registration_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_field_options_registration_field_id_registration_fields_id_fk": { + "name": "registration_field_options_registration_field_id_registration_fields_id_fk", + "tableFrom": "registration_field_options", + "tableTo": "registration_fields", + "columnsFrom": [ + "registration_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "question_options": { + "name": "question_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "registration_id": { + "name": "registration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_title": { + "name": "option_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "option_sub_title": { + "name": "option_sub_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "accepted": { + "name": "accepted", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vote_score": { + "name": "vote_score", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0.0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "question_options_user_id_users_id_fk": { + "name": "question_options_user_id_users_id_fk", + "tableFrom": "question_options", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "question_options_registration_id_registrations_id_fk": { + "name": "question_options_registration_id_registrations_id_fk", + "tableFrom": "question_options", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "question_options_question_id_forum_questions_id_fk": { + "name": "question_options_question_id_forum_questions_id_fk", + "tableFrom": "question_options", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "votes": { + "name": "votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_id": { + "name": "option_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "num_of_votes": { + "name": "num_of_votes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "votes_user_id_users_id_fk": { + "name": "votes_user_id_users_id_fk", + "tableFrom": "votes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_option_id_question_options_id_fk": { + "name": "votes_option_id_question_options_id_fk", + "tableFrom": "votes", + "tableTo": "question_options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_question_id_forum_questions_id_fk": { + "name": "votes_question_id_forum_questions_id_fk", + "tableFrom": "votes", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_fields": { + "name": "registration_fields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'TEXT'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_option_type": { + "name": "question_option_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "fields_display_rank": { + "name": "fields_display_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "character_limit": { + "name": "character_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_fields_event_id_events_id_fk": { + "name": "registration_fields_event_id_events_id_fk", + "tableFrom": "registration_fields", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registration_fields_question_id_forum_questions_id_fk": { + "name": "registration_fields_question_id_forum_questions_id_fk", + "tableFrom": "registration_fields", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_data": { + "name": "registration_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "registration_field_id": { + "name": "registration_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_data_registration_id_registrations_id_fk": { + "name": "registration_data_registration_id_registrations_id_fk", + "tableFrom": "registration_data", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registration_data_registration_field_id_registration_fields_id_fk": { + "name": "registration_data_registration_field_id_registration_fields_id_fk", + "tableFrom": "registration_data", + "tableTo": "registration_fields", + "columnsFrom": [ + "registration_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users_to_groups": { + "name": "users_to_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_groups_user_id_users_id_fk": { + "name": "users_to_groups_user_id_users_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_groups_group_id_groups_id_fk": { + "name": "users_to_groups_group_id_groups_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_groups_group_category_id_group_categories_id_fk": { + "name": "users_to_groups_group_category_id_group_categories_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_attributes": { + "name": "user_attributes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attribute_key": { + "name": "attribute_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "attribute_value": { + "name": "attribute_value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_attributes_user_id_users_id_fk": { + "name": "user_attributes_user_id_users_id_fk", + "tableFrom": "user_attributes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "likes_user_id_users_id_fk": { + "name": "likes_user_id_users_id_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "likes_comment_id_comments_id_fk": { + "name": "likes_comment_id_comments_id_fk", + "tableFrom": "likes", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "notification_types": { + "name": "notification_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "value": { + "name": "value", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notification_types_value_unique": { + "name": "notification_types_value_unique", + "nullsNotDistinct": false, + "columns": [ + "value" + ] + } + } + }, + "users_to_notifications": { + "name": "users_to_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notification_type_id": { + "name": "notification_type_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_notifications_user_id_users_id_fk": { + "name": "users_to_notifications_user_id_users_id_fk", + "tableFrom": "users_to_notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_notifications_notification_type_id_notification_types_id_fk": { + "name": "users_to_notifications_notification_type_id_notification_types_id_fk", + "tableFrom": "users_to_notifications", + "tableTo": "notification_types", + "columnsFrom": [ + "notification_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "questions_to_group_categories": { + "name": "questions_to_group_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "questions_to_group_categories_question_id_forum_questions_id_fk": { + "name": "questions_to_group_categories_question_id_forum_questions_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "questions_to_group_categories_group_category_id_group_categories_id_fk": { + "name": "questions_to_group_categories_group_category_id_group_categories_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 8c821548..e9ea0cf2 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -106,6 +106,13 @@ "when": 1712178506955, "tag": "0014_foamy_monster_badoon", "breakpoints": true + }, + { + "idx": 15, + "version": "5", + "when": 1712758321061, + "tag": "0015_sad_thunderbolt_ross", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/forumQuestions.ts b/src/db/forumQuestions.ts index bbc6d39e..c9602aba 100644 --- a/src/db/forumQuestions.ts +++ b/src/db/forumQuestions.ts @@ -2,6 +2,7 @@ import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import { cycles } from './cycles'; import { relations } from 'drizzle-orm'; import { questionOptions } from './questionOptions'; +import { questionsToGroupCategories } from './questionsToGroupCategories'; export const forumQuestions = pgTable('forum_questions', { id: uuid('id').primaryKey().defaultRandom(), @@ -20,6 +21,7 @@ export const forumQuestionsRelations = relations(forumQuestions, ({ one, many }) references: [cycles.id], }), questionOptions: many(questionOptions), + questionsToGroupCategories: many(questionsToGroupCategories), })); export type ForumQuestion = typeof forumQuestions.$inferSelect; diff --git a/src/db/groupCategories.ts b/src/db/groupCategories.ts index 32369144..6b548242 100644 --- a/src/db/groupCategories.ts +++ b/src/db/groupCategories.ts @@ -3,6 +3,7 @@ import { events } from './events'; import { groups } from './groups'; import { usersToGroups } from './usersToGroups'; import { relations } from 'drizzle-orm'; +import { questionsToGroupCategories } from './questionsToGroupCategories'; export const groupCategories = pgTable('group_categories', { id: uuid('id').primaryKey().defaultRandom(), @@ -19,6 +20,7 @@ export const groupCategoriesRelations = relations(groupCategories, ({ one, many }), group: many(groups), usersToGroup: many(usersToGroups), + questionsToGroupCategories: many(questionsToGroupCategories), })); export type GroupCategory = typeof groupCategories.$inferSelect; diff --git a/src/db/index.ts b/src/db/index.ts index 654193d3..30640a3b 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -17,3 +17,4 @@ export * from './likes'; export * from './notificationTypes'; export * from './usersToNotifications'; export * from './groupCategories'; +export * from './questionsToGroupCategories'; diff --git a/src/db/questionsToGroupCategories.ts b/src/db/questionsToGroupCategories.ts new file mode 100644 index 00000000..c2abfb90 --- /dev/null +++ b/src/db/questionsToGroupCategories.ts @@ -0,0 +1,30 @@ +import { pgTable, timestamp, uuid } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; +import { groupCategories } from './groupCategories'; +import { forumQuestions } from './forumQuestions'; + +export const questionsToGroupCategories = pgTable('questions_to_group_categories', { + id: uuid('id').primaryKey().defaultRandom(), + questionId: uuid('question_id') + .notNull() + .references(() => forumQuestions.id), + groupCategoryId: uuid('group_category_id').references(() => groupCategories.id), // Must be nullable (for now) because affiliation does not have a group category id. + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), +}); + +export const questionsToGroupCategoriesRelations = relations( + questionsToGroupCategories, + ({ one }) => ({ + question: one(forumQuestions, { + fields: [questionsToGroupCategories.questionId], + references: [forumQuestions.id], + }), + groupCategory: one(groupCategories, { + fields: [questionsToGroupCategories.groupCategoryId], + references: [groupCategories.id], + }), + }), +); + +export type QuestionsToGroupCategories = typeof questionsToGroupCategories.$inferSelect; From f7eb342d290d410314c35e81bf024cd9b73e66aa Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Wed, 10 Apr 2024 12:18:21 -0500 Subject: [PATCH 02/21] Add group id to registrations (#307) * add new handler to get group registration * remove scripts that will not be used * move getting registration data to its own router * refactor api endpoints * fix tests * fix another test * improve error message * remove migration * merge migrations --- assets/groups.example.csv | 4 - ...t_ross.sql => 0015_shiny_black_knight.sql} | 7 + migrations/meta/0015_snapshot.json | 21 ++- migrations/meta/_journal.json | 4 +- scripts/db/insertCustomGroups.ts | 53 ------- src/db/groups.ts | 2 + src/db/registrations.ts | 6 + src/handlers/events.ts | 66 --------- src/handlers/groups.ts | 17 +++ src/handlers/registrations.ts | 99 +++++++++++++ src/routers/events.ts | 10 +- src/routers/groups.ts | 3 +- src/routers/registrations.ts | 19 +++ src/services/registrationData.spec.ts | 4 +- src/services/registrations.spec.ts | 6 +- src/services/registrations.ts | 131 +++++++++++++----- 16 files changed, 280 insertions(+), 172 deletions(-) delete mode 100644 assets/groups.example.csv rename migrations/{0015_sad_thunderbolt_ross.sql => 0015_shiny_black_knight.sql} (72%) delete mode 100644 scripts/db/insertCustomGroups.ts create mode 100644 src/handlers/registrations.ts create mode 100644 src/routers/registrations.ts diff --git a/assets/groups.example.csv b/assets/groups.example.csv deleted file mode 100644 index ec7870c0..00000000 --- a/assets/groups.example.csv +++ /dev/null @@ -1,4 +0,0 @@ -name -Group A -Group B -Group C \ No newline at end of file diff --git a/migrations/0015_sad_thunderbolt_ross.sql b/migrations/0015_shiny_black_knight.sql similarity index 72% rename from migrations/0015_sad_thunderbolt_ross.sql rename to migrations/0015_shiny_black_knight.sql index b3a99c91..269ca6c1 100644 --- a/migrations/0015_sad_thunderbolt_ross.sql +++ b/migrations/0015_shiny_black_knight.sql @@ -6,6 +6,13 @@ CREATE TABLE IF NOT EXISTS "questions_to_group_categories" ( "updated_at" timestamp DEFAULT now() NOT NULL ); --> statement-breakpoint +ALTER TABLE "registrations" ADD COLUMN "group_id" uuid;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "registrations" ADD CONSTRAINT "registrations_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "groups"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint DO $$ BEGIN ALTER TABLE "questions_to_group_categories" ADD CONSTRAINT "questions_to_group_categories_question_id_forum_questions_id_fk" FOREIGN KEY ("question_id") REFERENCES "forum_questions"("id") ON DELETE no action ON UPDATE no action; EXCEPTION diff --git a/migrations/meta/0015_snapshot.json b/migrations/meta/0015_snapshot.json index 9217bb97..9485f110 100644 --- a/migrations/meta/0015_snapshot.json +++ b/migrations/meta/0015_snapshot.json @@ -1,5 +1,5 @@ { - "id": "d8506015-c303-43f5-8f18-20673b1869c2", + "id": "52b7a687-9af1-4d59-8443-195f60d1d1f5", "prevId": "2bbf7b5d-a98a-4170-8e2c-df6dcbab7133", "version": "5", "dialect": "pg", @@ -578,6 +578,12 @@ "primaryKey": false, "notNull": true }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "status": { "name": "status", "type": "varchar", @@ -627,6 +633,19 @@ ], "onDelete": "no action", "onUpdate": "no action" + }, + "registrations_group_id_groups_id_fk": { + "name": "registrations_group_id_groups_id_fk", + "tableFrom": "registrations", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index e9ea0cf2..41f8d3ba 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -110,8 +110,8 @@ { "idx": 15, "version": "5", - "when": 1712758321061, - "tag": "0015_sad_thunderbolt_ross", + "when": 1712769381020, + "tag": "0015_shiny_black_knight", "breakpoints": true } ] diff --git a/scripts/db/insertCustomGroups.ts b/scripts/db/insertCustomGroups.ts deleted file mode 100644 index fc2eb5e3..00000000 --- a/scripts/db/insertCustomGroups.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { createDbPool } from '../../src/utils/db/createDbPool'; -import * as fs from 'fs'; -import csvParser from 'csv-parser'; -import { insertCustomGroups } from '../../src/utils/db/insertCustomGroups'; - -const DEFAULT_DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432'; -const CSV_FILE_PATH = 'assets/groups.csv'; - -async function main() { - console.log(process.env.DB_CONNECTION_URL); - const dbConnectionUrl = process.env.DB_CONNECTION_URL ?? DEFAULT_DB_CONNECTION_URL; - const { dbPool, connection } = createDbPool(dbConnectionUrl, { max: 1 }); - - try { - const names: string[] = []; - try { - const stream = fs.createReadStream(CSV_FILE_PATH); - await new Promise((resolve, reject) => { - stream - .pipe(csvParser()) - .on('data', (row) => { - if (row.name) { - names.push(row.name.trim()); - } - }) - .on('end', () => { - console.log('Number of names:', names.length); - console.log('Names:', names); - resolve(); - }) - .on('error', (error) => { - reject(new Error(`Error reading CSV file: ${error}`)); - }); - }); - } catch (error) { - throw new Error(`Error processing CSV file: ${error}`); - } - - await insertCustomGroups(dbPool, names); - console.log('Inserted groups into the database'); - } catch (error) { - console.error('Error processing groups:', error); - } finally { - await connection.end(); - } -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error('Error seeding database:', error); - process.exit(1); - }); diff --git a/src/db/groups.ts b/src/db/groups.ts index fd4e469b..af8f79d7 100644 --- a/src/db/groups.ts +++ b/src/db/groups.ts @@ -2,6 +2,7 @@ import { relations } from 'drizzle-orm'; import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import { usersToGroups } from './usersToGroups'; import { groupCategories } from './groupCategories'; +import { registrations } from './registrations'; export const groups = pgTable('groups', { id: uuid('id').primaryKey().defaultRandom(), @@ -17,6 +18,7 @@ export const groupsRelations = relations(groups, ({ one, many }) => ({ fields: [groups.groupCategoryId], references: [groupCategories.id], }), + registrations: many(registrations), usersToGroups: many(usersToGroups), })); diff --git a/src/db/registrations.ts b/src/db/registrations.ts index 26107c41..143242f8 100644 --- a/src/db/registrations.ts +++ b/src/db/registrations.ts @@ -3,6 +3,7 @@ import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import { events } from './events'; import { registrationData } from './registrationData'; import { users } from './users'; +import { groups } from './groups'; export const registrations = pgTable('registrations', { id: uuid('id').primaryKey().defaultRandom(), @@ -12,6 +13,7 @@ export const registrations = pgTable('registrations', { eventId: uuid('event_id') .references(() => events.id) .notNull(), + groupId: uuid('group_id').references(() => groups.id), // CAN BE: DRAFT, APPROVED, REJECTED AND MORE status: varchar('status').default('DRAFT'), createdAt: timestamp('created_at').notNull().defaultNow(), @@ -27,6 +29,10 @@ export const registrationsRelations = relations(registrations, ({ one, many }) = fields: [registrations.eventId], references: [events.id], }), + group: one(groups, { + fields: [registrations.groupId], + references: [groups.id], + }), registrationData: many(registrationData), })); diff --git a/src/handlers/events.ts b/src/handlers/events.ts index d55f0718..42111d0d 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -2,9 +2,6 @@ import { and, eq } from 'drizzle-orm'; import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import type { Request, Response } from 'express'; import * as db from '../db'; -import { insertRegistrationSchema } from '../types'; -import { validateRequiredRegistrationFields } from '../services/registrationFields'; -import { saveEventRegistration } from '../services/registrations'; export function getEventCyclesHandler(dbPool: PostgresJsDatabase) { return async function (req: Request, res: Response) { @@ -76,69 +73,6 @@ export function getEventRegistrationFieldsHandler(dbPool: PostgresJsDatabase) { - return async function (req: Request, res: Response) { - const eventId = req.params.eventId; - const userId = req.session.userId; - if (!userId) { - return res.status(400).json({ errors: ['userId is required'] }); - } - - if (!eventId) { - return res.status(400).json({ errors: ['eventId is required'] }); - } - - try { - const event = await dbPool.query.events.findFirst({ - with: { - registrations: { - with: { - registrationData: true, - }, - where: (fields, { eq }) => eq(fields.userId, userId), - }, - }, - where: (fields, { eq }) => eq(fields.id, eventId), - }); - - const out = event?.registrations.map((registration) => registration.registrationData).flat(); - - return res.json({ data: out }); - } catch (e) { - return res.status(500).json({ errors: ['Failed to get registration data'] }); - } - }; -} - -export function saveEventRegistrationHandler(dbPool: PostgresJsDatabase) { - return async function (req: Request, res: Response) { - // parse input - const eventId = req.params.eventId; - const userId = req.session.userId; - req.body.userId = userId; - req.body.eventId = eventId; - const body = insertRegistrationSchema.safeParse(req.body); - - if (!body.success) { - return res.status(400).json({ errors: body.error.issues }); - } - - const missingRequiredFields = await validateRequiredRegistrationFields(dbPool, body.data); - - if (missingRequiredFields.length > 0) { - return res.status(400).json({ errors: missingRequiredFields }); - } - - try { - const out = await saveEventRegistration(dbPool, body.data, userId); - return res.json({ data: out }); - } catch (e) { - console.log('error saving registration ' + e); - return res.sendStatus(500); - } - }; -} - export function getEventRegistrationHandler(dbPool: PostgresJsDatabase) { return async function (req: Request, res: Response) { // parse input diff --git a/src/handlers/groups.ts b/src/handlers/groups.ts index 42bcb8dd..0f4db426 100644 --- a/src/handlers/groups.ts +++ b/src/handlers/groups.ts @@ -1,6 +1,7 @@ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import type { Request, Response } from 'express'; import * as db from '../db'; +import { eq } from 'drizzle-orm'; /** * Retrieves all groups from the database. * @param dbPool The database connection pool. @@ -12,3 +13,19 @@ export function getGroupsHandler(dbPool: PostgresJsDatabase) { return res.json({ data: groups }); }; } + +export function getGroupRegistrationsHandler(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + const groupId = req.params.id; + + if (!groupId) { + return res.status(400).json({ error: 'Group ID is required' }); + } + + const registrations = await dbPool.query.registrations.findMany({ + where: eq(db.registrations.groupId, groupId), + }); + + return res.json({ data: registrations }); + }; +} diff --git a/src/handlers/registrations.ts b/src/handlers/registrations.ts new file mode 100644 index 00000000..7b4dce1e --- /dev/null +++ b/src/handlers/registrations.ts @@ -0,0 +1,99 @@ +import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import type { Request, Response } from 'express'; +import * as db from '../db'; +import { insertRegistrationSchema } from '../types'; +import { validateRequiredRegistrationFields } from '../services/registrationFields'; +import { saveRegistration, updateRegistration } from '../services/registrations'; + +export function getRegistrationDataHandler(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + const registrationId = req.params.id; + const userId = req.session.userId; + if (!userId) { + return res.status(400).json({ errors: ['userId is required'] }); + } + + if (!registrationId) { + return res.status(400).json({ errors: ['registrationId is required'] }); + } + + try { + const registration = await dbPool.query.registrations.findFirst({ + with: { + registrationData: true, + }, + where: (fields, { eq, and }) => + and(eq(fields.userId, userId), eq(fields.id, registrationId)), + }); + + const out = registration; + + return res.json({ data: out }); + } catch (e) { + return res.status(500).json({ errors: ['Failed to get registration data'] }); + } + }; +} + +export function saveRegistrationHandler(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + const userId = req.session.userId; + req.body.userId = userId; + const body = insertRegistrationSchema.safeParse(req.body); + + if (!body.success) { + return res.status(400).json({ errors: body.error.issues }); + } + + const missingRequiredFields = await validateRequiredRegistrationFields(dbPool, body.data); + + if (missingRequiredFields.length > 0) { + return res.status(400).json({ errors: missingRequiredFields }); + } + + try { + const out = await saveRegistration(dbPool, body.data, userId); + return res.json({ data: out }); + } catch (e) { + console.log('error saving registration ' + e); + return res.sendStatus(500); + } + }; +} + +export function updateRegistrationHandler(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + const registrationId = req.params.id; + + if (!registrationId) { + return res.status(400).json({ errors: ['registrationId is required'] }); + } + + const userId = req.session.userId; + req.body.userId = userId; + const body = insertRegistrationSchema.safeParse(req.body); + + if (!body.success) { + return res.status(400).json({ errors: body.error.issues }); + } + + const missingRequiredFields = await validateRequiredRegistrationFields(dbPool, body.data); + + if (missingRequiredFields.length > 0) { + return res.status(400).json({ errors: missingRequiredFields }); + } + + try { + const out = await updateRegistration({ + data: body.data, + dbPool, + registrationId, + userId, + }); + return res.json({ data: out }); + } catch (e) { + console.log('error saving registration ' + e); + return res.sendStatus(500); + } + }; +} diff --git a/src/routers/events.ts b/src/routers/events.ts index 5bdf4667..f28186b2 100644 --- a/src/routers/events.ts +++ b/src/routers/events.ts @@ -5,11 +5,9 @@ import { isLoggedIn } from '../middleware/isLoggedIn'; import { getEventCyclesHandler, getEventHandler, - getEventRegistrationDataHandler, getEventRegistrationFieldsHandler, getEventRegistrationHandler, getEventsHandler, - saveEventRegistrationHandler, } from '../handlers/events'; const router = express.Router(); @@ -21,13 +19,7 @@ export function eventsRouter({ dbPool }: { dbPool: PostgresJsDatabase isLoggedIn(dbPool), getEventRegistrationFieldsHandler(dbPool), ); - router.get( - '/:eventId/registration-data', - isLoggedIn(dbPool), - getEventRegistrationDataHandler(dbPool), - ); router.get('/:eventId/cycles', isLoggedIn(dbPool), getEventCyclesHandler(dbPool)); - router.post('/:eventId/registration', isLoggedIn(dbPool), saveEventRegistrationHandler(dbPool)); - router.get('/:eventId/registration', isLoggedIn(dbPool), getEventRegistrationHandler(dbPool)); + router.get('/:eventId/registrations', isLoggedIn(dbPool), getEventRegistrationHandler(dbPool)); return router; } diff --git a/src/routers/groups.ts b/src/routers/groups.ts index 26358955..60cde854 100644 --- a/src/routers/groups.ts +++ b/src/routers/groups.ts @@ -2,10 +2,11 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { default as express } from 'express'; import type * as db from '../db'; import { isLoggedIn } from '../middleware/isLoggedIn'; -import { getGroupsHandler } from '../handlers/groups'; +import { getGroupRegistrationsHandler, getGroupsHandler } from '../handlers/groups'; const router = express.Router(); export function groupsRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { router.get('/', isLoggedIn(dbPool), getGroupsHandler(dbPool)); + router.get('/:id/registrations', isLoggedIn(dbPool), getGroupRegistrationsHandler(dbPool)); return router; } diff --git a/src/routers/registrations.ts b/src/routers/registrations.ts new file mode 100644 index 00000000..7eaea8d8 --- /dev/null +++ b/src/routers/registrations.ts @@ -0,0 +1,19 @@ +import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import { default as express } from 'express'; +import type * as db from '../db'; +import { isLoggedIn } from '../middleware/isLoggedIn'; +import { + getRegistrationDataHandler, + saveRegistrationHandler, + updateRegistrationHandler, +} from '../handlers/registrations'; + +const router = express.Router(); + +export function registrationsRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { + router.post('/', isLoggedIn(dbPool), saveRegistrationHandler(dbPool)); + router.put('/:id', isLoggedIn(dbPool), updateRegistrationHandler(dbPool)); + router.get('/:id/registration-data', isLoggedIn(dbPool), getRegistrationDataHandler(dbPool)); + + return router; +} diff --git a/src/services/registrationData.spec.ts b/src/services/registrationData.spec.ts index 37b296db..08207e7f 100644 --- a/src/services/registrationData.spec.ts +++ b/src/services/registrationData.spec.ts @@ -11,7 +11,7 @@ import { fetchRegistrationFields, filterRegistrationData, } from './registrationData'; -import { saveEventRegistration } from './registrations'; +import { saveRegistration } from './registrations'; import { isNotNull } from 'drizzle-orm'; const DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432'; @@ -65,7 +65,7 @@ describe('service: registrationData', () => { .update(db.registrationFields) .set({ questionId: forumQuestion?.id ?? '' }) .where(isNotNull(db.registrationFields.questionOptionType)); - registration = await saveEventRegistration(dbPool, testRegistration, testRegistration.userId); + registration = await saveRegistration(dbPool, testRegistration, testRegistration.userId); }); test('should update existing records', async () => { diff --git a/src/services/registrations.spec.ts b/src/services/registrations.spec.ts index 5eb70ffe..c0e46cad 100644 --- a/src/services/registrations.spec.ts +++ b/src/services/registrations.spec.ts @@ -3,7 +3,7 @@ import * as db from '../db'; import { createDbPool } from '../utils/db/createDbPool'; import postgres from 'postgres'; import { runMigrations } from '../utils/db/runMigrations'; -import { saveEventRegistration } from './registrations'; +import { saveRegistration } from './registrations'; import { z } from 'zod'; import { insertRegistrationSchema } from '../types'; import { cleanup, seed } from '../utils/db/seed'; @@ -37,7 +37,7 @@ describe('service: registrations', () => { }); test('send registration data', async function () { // Call the saveRegistration function - const response = await saveEventRegistration(dbPool, testData, testData.userId); + const response = await saveRegistration(dbPool, testData, testData.userId); // Check if response is defined expect(response).toBeDefined(); // Check property existence and types @@ -66,7 +66,7 @@ describe('service: registrations', () => { }, ]; // Call the saveRegistration function - const response = await saveEventRegistration(dbPool, testData, testData.userId); + const response = await saveRegistration(dbPool, testData, testData.userId); // Check if response is defined expect(response).toBeDefined(); // Check property existence and types diff --git a/src/services/registrations.ts b/src/services/registrations.ts index dd1d1d08..8cb255fe 100644 --- a/src/services/registrations.ts +++ b/src/services/registrations.ts @@ -9,15 +9,23 @@ import { upsertQuestionOptionFromRegistrationData, } from './registrationData'; -export async function saveEventRegistration( +export async function saveRegistration( dbPool: PostgresJsDatabase, data: z.infer, userId: string, ) { - const existingRegistration = await dbPool.query.registrations.findFirst({ - where: and(eq(db.registrations.userId, userId), eq(db.registrations.eventId, data.eventId)), - }); - const newRegistration = await upsertRegistration(dbPool, existingRegistration, data); + if (data.groupId) { + const userGroup = dbPool.query.usersToGroups.findFirst({ + where: (fields, { eq, and }) => + and(eq(fields.userId, userId), eq(fields.groupId, data.groupId!)), + }); + + if (!userGroup) { + throw new Error('user is not in group'); + } + } + + const newRegistration = await createRegistrationInDB(dbPool, data); if (!newRegistration) { throw new Error('failed to save registration'); } @@ -43,33 +51,94 @@ export async function saveEventRegistration( } } -async function upsertRegistration( +export async function updateRegistration({ + data, + dbPool, + registrationId, + userId, +}: { + dbPool: PostgresJsDatabase; + data: z.infer; + registrationId: string; + userId: string; +}) { + if (data.groupId) { + const userGroup = dbPool.query.usersToGroups.findFirst({ + where: (fields, { eq, and }) => + and(eq(fields.userId, userId), eq(fields.groupId, data.groupId!)), + }); + + if (!userGroup) { + throw new Error('user is not in group'); + } + } + + const existingRegistration = await dbPool.query.registrations.findFirst({ + where: and(eq(db.registrations.userId, userId), eq(db.registrations.id, registrationId)), + }); + + if (!existingRegistration) { + throw new Error('registration not found'); + } + + const updatedRegistration = await updateRegistrationInDB(dbPool, existingRegistration, data); + + if (!updatedRegistration) { + throw new Error('failed to save registration'); + } + + const updatedRegistrationData = await upsertRegistrationData({ + dbPool, + registrationId: updatedRegistration.id, + + registrationData: data.registrationData, + }); + + try { + await upsertQuestionOptionFromRegistrationData(dbPool, userId, updatedRegistrationData); + + const out = { + ...updatedRegistration, + registrationData: updatedRegistrationData, + }; + + return out; + } catch (error) { + console.error('Error in updateQuestionOptions: ', error); + throw new Error('Failed to update question options'); + } +} + +async function createRegistrationInDB( dbPool: PostgresJsDatabase, - registration: db.Registration | undefined, body: z.infer, ) { - if (registration) { - const updatedRegistration = await dbPool - .update(db.registrations) - .set({ - userId: body.userId, - eventId: body.eventId, - status: body.status, - updatedAt: new Date(), - }) - .where(eq(db.registrations.id, registration.id)) - .returning(); - return updatedRegistration[0]; - } else { - // insert to registration table - const newRegistration = await dbPool - .insert(db.registrations) - .values({ - userId: body.userId, - eventId: body.eventId, - status: body.status, - }) - .returning(); - return newRegistration[0]; - } + // insert to registration table + const newRegistration = await dbPool + .insert(db.registrations) + .values({ + userId: body.userId, + eventId: body.eventId, + status: body.status, + }) + .returning(); + return newRegistration[0]; +} + +async function updateRegistrationInDB( + dbPool: PostgresJsDatabase, + registration: db.Registration, + body: z.infer, +) { + const updatedRegistration = await dbPool + .update(db.registrations) + .set({ + userId: body.userId, + eventId: body.eventId, + status: body.status, + updatedAt: new Date(), + }) + .where(eq(db.registrations.id, registration.id)) + .returning(); + return updatedRegistration[0]; } From f43043603e6db9fcf8331b5ea6f4c9667d6e0fd6 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Wed, 10 Apr 2024 16:45:06 -0500 Subject: [PATCH 03/21] add registrations router to api router --- src/routers/api.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routers/api.ts b/src/routers/api.ts index cfdd6fba..e5b40cbc 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -12,6 +12,7 @@ import { groupsRouter } from './groups'; import { commentsRouter } from './comments'; import { optionsRouter } from './options'; import { votesRouter } from './votes'; +import { registrationsRouter } from './registrations'; const router = express.Router(); @@ -54,6 +55,7 @@ export function apiRouter({ router.use('/groups', groupsRouter({ dbPool })); router.use('/comments', commentsRouter({ dbPool })); router.use('/options', optionsRouter({ dbPool })); + router.use('/registrations', registrationsRouter({ dbPool })); return router; } From 2f4d4398e8d5939534be842e30874e5e7e3b9ac3 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Thu, 11 Apr 2024 07:31:29 -0500 Subject: [PATCH 04/21] add router for categories (#310) Co-authored-by: Martin Benedikt Busch <43137759+MartinBenediktBusch@users.noreply.github.com> --- src/handlers/groupCategories.ts | 27 +++++++++++++++++++++++++++ src/handlers/users.ts | 6 +++++- src/routers/api.ts | 1 + src/routers/groupCategories.ts | 14 ++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/handlers/groupCategories.ts create mode 100644 src/routers/groupCategories.ts diff --git a/src/handlers/groupCategories.ts b/src/handlers/groupCategories.ts new file mode 100644 index 00000000..283e6f8b --- /dev/null +++ b/src/handlers/groupCategories.ts @@ -0,0 +1,27 @@ +import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import type { Request, Response } from 'express'; +import * as db from '../db'; +import { eq } from 'drizzle-orm'; + +export function getGroupCategoriesHandler(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + const groupCategories = await dbPool.query.groupCategories.findMany(); + return res.json({ data: groupCategories }); + }; +} + +export function getGroupCategoryHandler(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + const groupCategoryId = req.params.id; + + if (!groupCategoryId) { + return res.status(400).json({ error: 'Group Category ID is required' }); + } + + const groupCategory = await dbPool.query.groupCategories.findFirst({ + where: eq(db.groupCategories.id, groupCategoryId), + }); + + return res.json({ data: groupCategory }); + }; +} diff --git a/src/handlers/users.ts b/src/handlers/users.ts index ec149464..bcd28504 100644 --- a/src/handlers/users.ts +++ b/src/handlers/users.ts @@ -95,7 +95,11 @@ export function getUserGroupsHandler(dbPool: PostgresJsDatabase) { try { const query = await dbPool.query.usersToGroups.findMany({ with: { - group: true, + group: { + with: { + groupCategory: true, + }, + }, }, where: eq(db.usersToGroups.userId, userId), }); diff --git a/src/routers/api.ts b/src/routers/api.ts index e5b40cbc..dbf2329b 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -55,6 +55,7 @@ export function apiRouter({ router.use('/groups', groupsRouter({ dbPool })); router.use('/comments', commentsRouter({ dbPool })); router.use('/options', optionsRouter({ dbPool })); + router.use('/group-categories', optionsRouter({ dbPool })); router.use('/registrations', registrationsRouter({ dbPool })); return router; diff --git a/src/routers/groupCategories.ts b/src/routers/groupCategories.ts new file mode 100644 index 00000000..e2de0ea2 --- /dev/null +++ b/src/routers/groupCategories.ts @@ -0,0 +1,14 @@ +import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import { default as express } from 'express'; +import type * as db from '../db'; +import { isLoggedIn } from '../middleware/isLoggedIn'; +import { getGroupCategoriesHandler, getGroupCategoryHandler } from '../handlers/groupCategories'; + +const router = express.Router(); + +export function groupCategoriesRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { + router.get('/', isLoggedIn(dbPool), getGroupCategoriesHandler(dbPool)); + router.get('/:id', isLoggedIn(dbPool), getGroupCategoryHandler(dbPool)); + + return router; +} From f9d1d916438383bfac581af17ce44170cfc3ad1a Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Thu, 11 Apr 2024 09:32:24 -0500 Subject: [PATCH 05/21] extract registration data from handler (#312) --- src/handlers/registrations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/registrations.ts b/src/handlers/registrations.ts index 7b4dce1e..217f5871 100644 --- a/src/handlers/registrations.ts +++ b/src/handlers/registrations.ts @@ -26,7 +26,7 @@ export function getRegistrationDataHandler(dbPool: PostgresJsDatabase and(eq(fields.userId, userId), eq(fields.id, registrationId)), }); - const out = registration; + const out = [...(registration?.registrationData ?? [])]; return res.json({ data: out }); } catch (e) { From e2eebb148c0b7e9d569855e1a0b481cba8896347 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Fri, 12 Apr 2024 09:41:15 -0500 Subject: [PATCH 06/21] fix only bringing one registration for an event (#316) --- src/handlers/events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/events.ts b/src/handlers/events.ts index 42111d0d..c582026c 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -73,14 +73,14 @@ export function getEventRegistrationFieldsHandler(dbPool: PostgresJsDatabase) { +export function getEventRegistrationsHandler(dbPool: PostgresJsDatabase) { return async function (req: Request, res: Response) { // parse input const eventId = req.params.eventId ?? ''; const userId = req.session.userId; try { - const out = await dbPool.query.registrations.findFirst({ + const out = await dbPool.query.registrations.findMany({ where: and(eq(db.registrations.userId, userId), eq(db.registrations.eventId, eventId)), }); From 8be6a350093d4366bf48aa7483c9ec45532b090a Mon Sep 17 00:00:00 2001 From: Camilo Vega <59750365+camilovegag@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:45:28 -0500 Subject: [PATCH 07/21] Fix typo in events.ts (#317) --- src/routers/events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routers/events.ts b/src/routers/events.ts index f28186b2..978b245d 100644 --- a/src/routers/events.ts +++ b/src/routers/events.ts @@ -6,7 +6,7 @@ import { getEventCyclesHandler, getEventHandler, getEventRegistrationFieldsHandler, - getEventRegistrationHandler, + getEventRegistrationsHandler, getEventsHandler, } from '../handlers/events'; const router = express.Router(); @@ -20,6 +20,6 @@ export function eventsRouter({ dbPool }: { dbPool: PostgresJsDatabase getEventRegistrationFieldsHandler(dbPool), ); router.get('/:eventId/cycles', isLoggedIn(dbPool), getEventCyclesHandler(dbPool)); - router.get('/:eventId/registrations', isLoggedIn(dbPool), getEventRegistrationHandler(dbPool)); + router.get('/:eventId/registrations', isLoggedIn(dbPool), getEventRegistrationsHandler(dbPool)); return router; } From 702208b190e7ea7a01b282bbab1d60314be2db38 Mon Sep 17 00:00:00 2001 From: Martin Benedikt Busch <43137759+MartinBenediktBusch@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:53:32 +0200 Subject: [PATCH 08/21] Update vote service to allow for different grouping dimensions when calculating the plurality score (#311) * update seed to create questions to group categories * fix tests * update vote service * fix existing tests * fix seed and update tests * test query group category ids * insert dummy uuid to allow IN in sql query * cleanup * remove unused var --- src/services/statistics.spec.ts | 2 +- src/services/votes.spec.ts | 60 +++++++++++++++++++++++++++++++-- src/services/votes.ts | 45 +++++++++++++++++++++++-- src/utils/db/seed.ts | 53 ++++++++++++++++++++++++++--- 4 files changed, 151 insertions(+), 9 deletions(-) diff --git a/src/services/statistics.spec.ts b/src/services/statistics.spec.ts index 4e333dd5..3fdf6c94 100644 --- a/src/services/statistics.spec.ts +++ b/src/services/statistics.spec.ts @@ -84,7 +84,7 @@ describe('service: statistics', () => { expect(optionStat?.distinctUsers).toEqual(2); expect(optionStat?.allocatedHearts).toEqual(8); expect(optionStat?.quadraticScore).toEqual('4'); - expect(optionStat?.distinctGroups).toEqual(1); + expect(optionStat?.distinctGroups).toEqual(2); const listOfGroupNames = optionStat?.listOfGroupNames; // Check if the array is not empty expect(listOfGroupNames).toBeDefined(); diff --git a/src/services/votes.spec.ts b/src/services/votes.spec.ts index 2aee4b92..48dbd9a5 100644 --- a/src/services/votes.spec.ts +++ b/src/services/votes.spec.ts @@ -9,6 +9,7 @@ import { z } from 'zod'; import { saveVote, queryVoteData, + queryGroupCategories, numOfVotesDictionary, groupsDictionary, calculatePluralScore, @@ -29,6 +30,9 @@ describe('service: votes', () => { let questionOption: db.QuestionOption | undefined; let otherQuestionOption: db.QuestionOption | undefined; let forumQuestion: db.ForumQuestion | undefined; + let otherForumQuestion: db.ForumQuestion | undefined; + let groupCategory: db.GroupCategory | undefined; + let otherGroupCategory: db.GroupCategory | undefined; let user: db.User | undefined; let secondUser: db.User | undefined; let thirdUser: db.User | undefined; @@ -38,11 +42,14 @@ describe('service: votes', () => { dbPool = initDb.dbPool; dbConnection = initDb.connection; // seed - const { users, questionOptions, forumQuestions, cycles } = await seed(dbPool); + const { users, questionOptions, forumQuestions, cycles, groupCategories } = await seed(dbPool); // Insert registration fields for the user questionOption = questionOptions[0]; otherQuestionOption = questionOptions[1]; forumQuestion = forumQuestions[0]; + otherForumQuestion = forumQuestions[1]; + groupCategory = groupCategories[0]; + otherGroupCategory = groupCategories[1]; user = users[0]; secondUser = users[1]; thirdUser = users[2]; @@ -182,11 +189,60 @@ describe('service: votes', () => { expect(thirdUser!.id in result).toBe(false); }); + test('that query group categories returns the correct amount of group category ids', async () => { + // Get vote data required for groups + const groupCategoriesIdArray = await queryGroupCategories(dbPool, forumQuestion!.id); + expect(groupCategoriesIdArray).toBeDefined(); + expect(groupCategoriesIdArray.length).toBe(2); + expect(Array.isArray(groupCategoriesIdArray)).toBe(true); + groupCategoriesIdArray.forEach((categoryId) => { + expect(typeof categoryId).toBe('string'); + }); + }); + + test('that query group categories returns an empty array if their are no group categories for a specific question', async () => { + // Get vote data required for groups + const groupCategoriesIdArray = await queryGroupCategories(dbPool, otherForumQuestion!.id); + expect(groupCategoriesIdArray).toBeDefined(); + expect(groupCategoriesIdArray.length).toBe(1); + expect(Array.isArray(groupCategoriesIdArray)).toBe(true); + expect(groupCategoriesIdArray).toEqual(['00000000-0000-0000-0000-000000000000']); + }); + test('only return groups for users who voted for the option', async () => { // Get vote data required for groups const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); const votesDictionary = await numOfVotesDictionary(voteArray); - const groups = await groupsDictionary(dbPool, votesDictionary); + const groups = await groupsDictionary(dbPool, votesDictionary, [groupCategory!.id]); + + expect(groups).toBeDefined(); + expect(groups['unexpectedKey']).toBeUndefined(); + expect(typeof groups).toBe('object'); + expect(Object.keys(groups).length).toEqual(2); + expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); + }); + + test('only return baseline groups for users who voted for the option as non of the users is in the additional group category', async () => { + // Get vote data required for groups + const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); + const votesDictionary = await numOfVotesDictionary(voteArray); + const groups = await groupsDictionary(dbPool, votesDictionary, [otherGroupCategory!.id]); + + expect(groups).toBeDefined(); + expect(groups['unexpectedKey']).toBeUndefined(); + expect(typeof groups).toBe('object'); + expect(Object.keys(groups).length).toEqual(1); + expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); + }); + + test('only return baseline groups when no addtional group category gets provided', async () => { + // Get vote data required for groups + const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); + const votesDictionary = await numOfVotesDictionary(voteArray); + + const groups = await groupsDictionary(dbPool, votesDictionary, [ + '00000000-0000-0000-0000-000000000000', + ]); expect(groups).toBeDefined(); expect(groups['unexpectedKey']).toBeUndefined(); diff --git a/src/services/votes.ts b/src/services/votes.ts index 1e068f8a..8dfb7329 100644 --- a/src/services/votes.ts +++ b/src/services/votes.ts @@ -78,13 +78,40 @@ export function numOfVotesDictionary(voteArray: Array<{ userId: string; numOfVot return numOfVotesDictionary; } +export async function queryGroupCategories( + dbPool: PostgresJsDatabase, + questionId: string, +): Promise { + const groupCategories = await dbPool + .select({ + groupCategoryId: db.questionsToGroupCategories.groupCategoryId, + }) + .from(db.questionsToGroupCategories) + .where(eq(db.questionsToGroupCategories.questionId, questionId)); + + // Need to due this adjustment because currently group_category_id is nullable due to affiliations having no label. + const groupCategoryIds = groupCategories + .map((category) => category.groupCategoryId) + .filter((id) => id !== null) as string[]; + + // Returning dummy uuid if there are no group categories found + if (groupCategoryIds.length === 0) { + return ['00000000-0000-0000-0000-000000000000']; + } + + return groupCategoryIds; +} + /** * Queries group data and creates group dictionary based on user IDs and option ID. - * @param {PostgresJsDatabase} dbPool - The database connection pool. + * @param {Record} numOfVotesDictionary - Dictionary of user IDs and their respective number of votes. + * @param {Array} groupCategoryIds - Array of group category IDs. + * @returns {Promise>} - Dictionary of group IDs and their corresponding user IDs. */ export async function groupsDictionary( dbPool: PostgresJsDatabase, numOfVotesDictionary: Record, + groupCategories: Array, ) { const groupArray = await dbPool.execute<{ groupId: string; userIds: string[] }>( sql.raw(` @@ -93,6 +120,9 @@ export async function groupsDictionary( WHERE user_id IN (${Object.keys(numOfVotesDictionary) .map((id) => `'${id}'`) .join(', ')}) + AND (group_category_id IN (${groupCategories + .map((category) => `'${category}'`) + .join(', ')}) OR group_category_id IS NULL) GROUP BY group_id `), ); @@ -171,8 +201,19 @@ export async function updateVoteScore( // Transform data const votesDictionary = await numOfVotesDictionary(voteArray); + // Query question Id + const queryQuestionId = await dbPool + .select({ + questionId: db.questionOptions.questionId, + }) + .from(db.questionOptions) + .where(eq(db.questionOptions.id, optionId)); + + // Query group categories + const groupCategories = await queryGroupCategories(dbPool, queryQuestionId[0]!.questionId); + // Query group data - const groupArray = await groupsDictionary(dbPool, votesDictionary); + const groupArray = await groupsDictionary(dbPool, votesDictionary, groupCategories ?? []); // Perform plural voting calculation const score = await calculatePluralScore(groupArray, votesDictionary); diff --git a/src/utils/db/seed.ts b/src/utils/db/seed.ts index 2c462274..c4e90188 100644 --- a/src/utils/db/seed.ts +++ b/src/utils/db/seed.ts @@ -17,6 +17,12 @@ async function seed(dbPool: PostgresJsDatabase) { groups.map((g) => g.id!), groupCategories[0]?.id, ); + const questionsToGroupCategories = await createQuestionsToGroupCategories( + dbPool, + forumQuestions[0]!.id, + groupCategories[0]?.id, + groupCategories[1]?.id, + ); return { events, @@ -28,6 +34,7 @@ async function seed(dbPool: PostgresJsDatabase) { users, usersToGroups, registrationFields, + questionsToGroupCategories, }; } @@ -42,6 +49,7 @@ async function cleanup(dbPool: PostgresJsDatabase) { await dbPool.delete(db.usersToGroups); await dbPool.delete(db.users); await dbPool.delete(db.groups); + await dbPool.delete(db.questionsToGroupCategories); await dbPool.delete(db.groupCategories); await dbPool.delete(db.forumQuestions); await dbPool.delete(db.cycles); @@ -114,10 +122,16 @@ async function createForumQuestions(dbPool: PostgresJsDatabase, cycle return dbPool .insert(db.forumQuestions) - .values({ - cycleId, - questionTitle: "What's your favorite movie?", - }) + .values([ + { + cycleId, + questionTitle: "What's your favorite movie?", + }, + { + cycleId, + questionTitle: 'What is your favorit fruit?', + }, + ]) .returning(); } @@ -206,7 +220,38 @@ async function createUsersToGroups( groupId: index < 2 ? groupIds[0]! : groupIds[1]!, groupCategoryId, })); + + // Add baseline group for each user (i.e. each user must be assigned to at least one group at all times) + userIds.forEach((userId) => { + usersToGroups.push({ + userId, + groupId: groupIds[3]!, + groupCategoryId: undefined, // udefined because currently affiliation does not have a group category id + }); + }); + return dbPool.insert(db.usersToGroups).values(usersToGroups).returning(); } +async function createQuestionsToGroupCategories( + dbPool: PostgresJsDatabase, + questionId: string, + groupCategoryIdOne?: string, + groupCategoryIdTwo?: string, +) { + return dbPool + .insert(db.questionsToGroupCategories) + .values([ + { + questionId: questionId, + groupCategoryId: groupCategoryIdOne, + }, + { + questionId: questionId, + groupCategoryId: groupCategoryIdTwo, + }, + ]) + .returning(); +} + export { seed, cleanup }; From c00587c99c0eac9715630a56ccceca1bed8c6b68 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Fri, 12 Apr 2024 09:56:09 -0500 Subject: [PATCH 09/21] Add secret code service (#313) * add migrations for secret * add create group type * update migrations for more control on user settings * create group handler * add join secret group handler * add group from category handler * add tests * add handler to router * fix rename issue --- migrations/0016_ambitious_rictor.sql | 4 + migrations/meta/0016_snapshot.json | 1555 ++++++++++++++++++++++++++ migrations/meta/_journal.json | 7 + src/db/groupCategories.ts | 4 +- src/db/groups.ts | 1 + src/handlers/groupCategories.ts | 25 + src/handlers/groups.ts | 41 + src/handlers/usersToGroups.ts | 32 + src/routers/api.ts | 2 + src/routers/groupCategories.ts | 7 +- src/routers/groups.ts | 8 +- src/routers/usersToGroups.ts | 12 + src/services/groupCategories.spec.ts | 85 ++ src/services/groupCategories.ts | 32 + src/services/groups.spec.ts | 65 ++ src/services/groups.ts | 86 +- src/types/groups.ts | 12 + src/types/usersToGroups.ts | 5 + 18 files changed, 1919 insertions(+), 64 deletions(-) create mode 100644 migrations/0016_ambitious_rictor.sql create mode 100644 migrations/meta/0016_snapshot.json create mode 100644 src/handlers/usersToGroups.ts create mode 100644 src/routers/usersToGroups.ts create mode 100644 src/services/groupCategories.spec.ts create mode 100644 src/services/groupCategories.ts create mode 100644 src/services/groups.spec.ts create mode 100644 src/types/groups.ts create mode 100644 src/types/usersToGroups.ts diff --git a/migrations/0016_ambitious_rictor.sql b/migrations/0016_ambitious_rictor.sql new file mode 100644 index 00000000..87a9aae3 --- /dev/null +++ b/migrations/0016_ambitious_rictor.sql @@ -0,0 +1,4 @@ +ALTER TABLE "group_categories" ADD COLUMN "user_can_create" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "group_categories" ADD COLUMN "user_can_view" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "groups" ADD COLUMN "secret" varchar(256);--> statement-breakpoint +ALTER TABLE "groups" ADD CONSTRAINT "groups_secret_unique" UNIQUE("secret"); \ No newline at end of file diff --git a/migrations/meta/0016_snapshot.json b/migrations/meta/0016_snapshot.json new file mode 100644 index 00000000..a084a595 --- /dev/null +++ b/migrations/meta/0016_snapshot.json @@ -0,0 +1,1555 @@ +{ + "id": "3bca2de9-9a2c-46ce-87c8-226969d43ec7", + "prevId": "52b7a687-9af1-4d59-8443-195f60d1d1f5", + "version": "5", + "dialect": "pg", + "tables": { + "comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_option_id": { + "name": "question_option_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comments_question_option_id_question_options_id_fk": { + "name": "comments_question_option_id_question_options_id_fk", + "tableFrom": "comments", + "tableTo": "question_options", + "columnsFrom": [ + "question_option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "cycles": { + "name": "cycles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'UPCOMING'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cycles_event_id_events_id_fk": { + "name": "cycles_event_id_events_id_fk", + "tableFrom": "cycles", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "registration_description": { + "name": "registration_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_display_rank": { + "name": "event_display_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "federated_credentials": { + "name": "federated_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federated_credentials_user_id_users_id_fk": { + "name": "federated_credentials_user_id_users_id_fk", + "tableFrom": "federated_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_subject_idx": { + "name": "provider_subject_idx", + "nullsNotDistinct": false, + "columns": [ + "provider", + "subject" + ] + } + } + }, + "forum_questions": { + "name": "forum_questions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_title": { + "name": "question_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "question_sub_title": { + "name": "question_sub_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "forum_questions_cycle_id_cycles_id_fk": { + "name": "forum_questions_cycle_id_cycles_id_fk", + "tableFrom": "forum_questions", + "tableTo": "cycles", + "columnsFrom": [ + "cycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "group_categories": { + "name": "group_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_can_create": { + "name": "user_can_create", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_can_view": { + "name": "user_can_view", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "group_categories_event_id_events_id_fk": { + "name": "group_categories_event_id_events_id_fk", + "tableFrom": "group_categories", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "groups": { + "name": "groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "groups_group_category_id_group_categories_id_fk": { + "name": "groups_group_category_id_group_categories_id_fk", + "tableFrom": "groups", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "groups_secret_unique": { + "name": "groups_secret_unique", + "nullsNotDistinct": false, + "columns": [ + "secret" + ] + } + } + }, + "users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram": { + "name": "telegram", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_telegram_unique": { + "name": "users_telegram_unique", + "nullsNotDistinct": false, + "columns": [ + "telegram" + ] + } + } + }, + "registrations": { + "name": "registrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'DRAFT'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registrations_user_id_users_id_fk": { + "name": "registrations_user_id_users_id_fk", + "tableFrom": "registrations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registrations_event_id_events_id_fk": { + "name": "registrations_event_id_events_id_fk", + "tableFrom": "registrations", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registrations_group_id_groups_id_fk": { + "name": "registrations_group_id_groups_id_fk", + "tableFrom": "registrations", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_field_options": { + "name": "registration_field_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_field_id": { + "name": "registration_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_field_options_registration_field_id_registration_fields_id_fk": { + "name": "registration_field_options_registration_field_id_registration_fields_id_fk", + "tableFrom": "registration_field_options", + "tableTo": "registration_fields", + "columnsFrom": [ + "registration_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "question_options": { + "name": "question_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "registration_id": { + "name": "registration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_title": { + "name": "option_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "option_sub_title": { + "name": "option_sub_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "accepted": { + "name": "accepted", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vote_score": { + "name": "vote_score", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0.0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "question_options_user_id_users_id_fk": { + "name": "question_options_user_id_users_id_fk", + "tableFrom": "question_options", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "question_options_registration_id_registrations_id_fk": { + "name": "question_options_registration_id_registrations_id_fk", + "tableFrom": "question_options", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "question_options_question_id_forum_questions_id_fk": { + "name": "question_options_question_id_forum_questions_id_fk", + "tableFrom": "question_options", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "votes": { + "name": "votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_id": { + "name": "option_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "num_of_votes": { + "name": "num_of_votes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "votes_user_id_users_id_fk": { + "name": "votes_user_id_users_id_fk", + "tableFrom": "votes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_option_id_question_options_id_fk": { + "name": "votes_option_id_question_options_id_fk", + "tableFrom": "votes", + "tableTo": "question_options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_question_id_forum_questions_id_fk": { + "name": "votes_question_id_forum_questions_id_fk", + "tableFrom": "votes", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_fields": { + "name": "registration_fields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'TEXT'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_option_type": { + "name": "question_option_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "fields_display_rank": { + "name": "fields_display_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "character_limit": { + "name": "character_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_fields_event_id_events_id_fk": { + "name": "registration_fields_event_id_events_id_fk", + "tableFrom": "registration_fields", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registration_fields_question_id_forum_questions_id_fk": { + "name": "registration_fields_question_id_forum_questions_id_fk", + "tableFrom": "registration_fields", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_data": { + "name": "registration_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "registration_field_id": { + "name": "registration_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_data_registration_id_registrations_id_fk": { + "name": "registration_data_registration_id_registrations_id_fk", + "tableFrom": "registration_data", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registration_data_registration_field_id_registration_fields_id_fk": { + "name": "registration_data_registration_field_id_registration_fields_id_fk", + "tableFrom": "registration_data", + "tableTo": "registration_fields", + "columnsFrom": [ + "registration_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users_to_groups": { + "name": "users_to_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_groups_user_id_users_id_fk": { + "name": "users_to_groups_user_id_users_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_groups_group_id_groups_id_fk": { + "name": "users_to_groups_group_id_groups_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_groups_group_category_id_group_categories_id_fk": { + "name": "users_to_groups_group_category_id_group_categories_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_attributes": { + "name": "user_attributes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attribute_key": { + "name": "attribute_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "attribute_value": { + "name": "attribute_value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_attributes_user_id_users_id_fk": { + "name": "user_attributes_user_id_users_id_fk", + "tableFrom": "user_attributes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "likes_user_id_users_id_fk": { + "name": "likes_user_id_users_id_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "likes_comment_id_comments_id_fk": { + "name": "likes_comment_id_comments_id_fk", + "tableFrom": "likes", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "notification_types": { + "name": "notification_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "value": { + "name": "value", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notification_types_value_unique": { + "name": "notification_types_value_unique", + "nullsNotDistinct": false, + "columns": [ + "value" + ] + } + } + }, + "users_to_notifications": { + "name": "users_to_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notification_type_id": { + "name": "notification_type_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_notifications_user_id_users_id_fk": { + "name": "users_to_notifications_user_id_users_id_fk", + "tableFrom": "users_to_notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_notifications_notification_type_id_notification_types_id_fk": { + "name": "users_to_notifications_notification_type_id_notification_types_id_fk", + "tableFrom": "users_to_notifications", + "tableTo": "notification_types", + "columnsFrom": [ + "notification_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "questions_to_group_categories": { + "name": "questions_to_group_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "questions_to_group_categories_question_id_forum_questions_id_fk": { + "name": "questions_to_group_categories_question_id_forum_questions_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "questions_to_group_categories_group_category_id_group_categories_id_fk": { + "name": "questions_to_group_categories_group_category_id_group_categories_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 41f8d3ba..b85575ec 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -113,6 +113,13 @@ "when": 1712769381020, "tag": "0015_shiny_black_knight", "breakpoints": true + }, + { + "idx": 16, + "version": "5", + "when": 1712848852970, + "tag": "0016_ambitious_rictor", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/groupCategories.ts b/src/db/groupCategories.ts index 6b548242..5e5bbc73 100644 --- a/src/db/groupCategories.ts +++ b/src/db/groupCategories.ts @@ -1,4 +1,4 @@ -import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; +import { pgTable, timestamp, uuid, varchar, boolean } from 'drizzle-orm/pg-core'; import { events } from './events'; import { groups } from './groups'; import { usersToGroups } from './usersToGroups'; @@ -9,6 +9,8 @@ export const groupCategories = pgTable('group_categories', { id: uuid('id').primaryKey().defaultRandom(), name: varchar('name'), eventId: uuid('event_id').references(() => events.id), + userCanCreate: boolean('user_can_create').notNull().default(false), + userCanView: boolean('user_can_view').notNull().default(false), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); diff --git a/src/db/groups.ts b/src/db/groups.ts index af8f79d7..708dbb0a 100644 --- a/src/db/groups.ts +++ b/src/db/groups.ts @@ -8,6 +8,7 @@ export const groups = pgTable('groups', { id: uuid('id').primaryKey().defaultRandom(), name: varchar('name', { length: 256 }).notNull(), description: varchar('description', { length: 256 }), + secret: varchar('secret', { length: 256 }).unique(), groupCategoryId: uuid('group_category_id').references(() => groupCategories.id), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), diff --git a/src/handlers/groupCategories.ts b/src/handlers/groupCategories.ts index 283e6f8b..be63a244 100644 --- a/src/handlers/groupCategories.ts +++ b/src/handlers/groupCategories.ts @@ -2,6 +2,7 @@ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import type { Request, Response } from 'express'; import * as db from '../db'; import { eq } from 'drizzle-orm'; +import { canViewGroupsInGroupCategory } from '../services/groupCategories'; export function getGroupCategoriesHandler(dbPool: PostgresJsDatabase) { return async function (req: Request, res: Response) { @@ -25,3 +26,27 @@ export function getGroupCategoryHandler(dbPool: PostgresJsDatabase) { return res.json({ data: groupCategory }); }; } + +export function getGroupCategoriesGroupsHandler(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + const groupCategoryId = req.params.id; + + if (!groupCategoryId) { + return res.status(400).json({ error: 'Group Category ID is required' }); + } + + const canView = await canViewGroupsInGroupCategory(dbPool, groupCategoryId); + + if (!canView) { + return res + .status(403) + .json({ error: 'You do not have permission to view this group category' }); + } + + const groups = await dbPool.query.groups.findMany({ + where: eq(db.groups.groupCategoryId, groupCategoryId), + }); + + return res.json({ data: groups }); + }; +} diff --git a/src/handlers/groups.ts b/src/handlers/groups.ts index 0f4db426..7fca8542 100644 --- a/src/handlers/groups.ts +++ b/src/handlers/groups.ts @@ -2,6 +2,10 @@ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import type { Request, Response } from 'express'; import * as db from '../db'; import { eq } from 'drizzle-orm'; +import { insertGroupsSchema } from '../types/groups'; +import { canCreateGroupInGroupCategory } from '../services/groupCategories'; +import { createSecretGroup } from '../services/groups'; +import { upsertUsersToGroups } from '../services/usersToGroups'; /** * Retrieves all groups from the database. * @param dbPool The database connection pool. @@ -29,3 +33,40 @@ export function getGroupRegistrationsHandler(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + const userId = req.session.userId; + const body = insertGroupsSchema.safeParse(req.body); + + if (!body.success) { + return res.status(400).json({ errors: body.error.errors }); + } + + try { + const canCreateGroup = await canCreateGroupInGroupCategory( + dbPool, + body.data.groupCategoryId!, + ); + + if (!canCreateGroup) { + return res + .status(403) + .json({ error: 'You do not have permission to create a group in this category' }); + } + + const newGroupRows = await createSecretGroup(dbPool, body.data); + + if (!newGroupRows || !newGroupRows[0]) { + return res.status(500).json({ error: 'An error occurred while creating the group' }); + } + + // assign user to new group + await upsertUsersToGroups(dbPool, userId, [newGroupRows[0].id]); + + return res.json({ data: newGroupRows[0] }); + } catch (error) { + return res.status(500).json({ error: 'An error occurred while creating the group' }); + } + }; +} diff --git a/src/handlers/usersToGroups.ts b/src/handlers/usersToGroups.ts new file mode 100644 index 00000000..1c8cc73a --- /dev/null +++ b/src/handlers/usersToGroups.ts @@ -0,0 +1,32 @@ +import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import type { Request, Response } from 'express'; +import * as db from '../db'; +import { joinGroupsSchema } from '../types/usersToGroups'; +import { getSecretGroup } from '../services/groups'; +import { upsertUsersToGroups } from '../services/usersToGroups'; + +export function joinGroupsHandler(dbPool: PostgresJsDatabase) { + return async (req: Request, res: Response) => { + const userId = req.session.userId; + const body = joinGroupsSchema.safeParse(req.body); + + if (!body.success) { + return res.status(400).json({ errors: body.error.errors }); + } + + try { + const secretGroup = await getSecretGroup(dbPool, body.data.secret); + + if (!secretGroup) { + return res.status(404).json({ error: 'Group not found' }); + } + + const userToGroup = await upsertUsersToGroups(dbPool, userId, [secretGroup.id]); + + return res.json({ data: userToGroup }); + } catch (e) { + console.error(e); + return res.status(500).json({ error: 'An error occurred while joining the group' }); + } + }; +} diff --git a/src/routers/api.ts b/src/routers/api.ts index dbf2329b..d093fe9f 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -13,6 +13,7 @@ import { commentsRouter } from './comments'; import { optionsRouter } from './options'; import { votesRouter } from './votes'; import { registrationsRouter } from './registrations'; +import { usersToGroupsRouter } from './usersToGroups'; const router = express.Router(); @@ -57,6 +58,7 @@ export function apiRouter({ router.use('/options', optionsRouter({ dbPool })); router.use('/group-categories', optionsRouter({ dbPool })); router.use('/registrations', registrationsRouter({ dbPool })); + router.use('/users-to-groups', usersToGroupsRouter({ dbPool })); return router; } diff --git a/src/routers/groupCategories.ts b/src/routers/groupCategories.ts index e2de0ea2..201a6c63 100644 --- a/src/routers/groupCategories.ts +++ b/src/routers/groupCategories.ts @@ -2,13 +2,18 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { default as express } from 'express'; import type * as db from '../db'; import { isLoggedIn } from '../middleware/isLoggedIn'; -import { getGroupCategoriesHandler, getGroupCategoryHandler } from '../handlers/groupCategories'; +import { + getGroupCategoriesGroupsHandler, + getGroupCategoriesHandler, + getGroupCategoryHandler, +} from '../handlers/groupCategories'; const router = express.Router(); export function groupCategoriesRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { router.get('/', isLoggedIn(dbPool), getGroupCategoriesHandler(dbPool)); router.get('/:id', isLoggedIn(dbPool), getGroupCategoryHandler(dbPool)); + router.get('/:id/groups', isLoggedIn(dbPool), getGroupCategoriesGroupsHandler(dbPool)); return router; } diff --git a/src/routers/groups.ts b/src/routers/groups.ts index 60cde854..e5059079 100644 --- a/src/routers/groups.ts +++ b/src/routers/groups.ts @@ -2,11 +2,17 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { default as express } from 'express'; import type * as db from '../db'; import { isLoggedIn } from '../middleware/isLoggedIn'; -import { getGroupRegistrationsHandler, getGroupsHandler } from '../handlers/groups'; +import { + createGroupHandler, + getGroupRegistrationsHandler, + getGroupsHandler, +} from '../handlers/groups'; const router = express.Router(); export function groupsRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { router.get('/', isLoggedIn(dbPool), getGroupsHandler(dbPool)); + router.post('/', isLoggedIn(dbPool), createGroupHandler(dbPool)); router.get('/:id/registrations', isLoggedIn(dbPool), getGroupRegistrationsHandler(dbPool)); + return router; } diff --git a/src/routers/usersToGroups.ts b/src/routers/usersToGroups.ts new file mode 100644 index 00000000..25fd87b9 --- /dev/null +++ b/src/routers/usersToGroups.ts @@ -0,0 +1,12 @@ +import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import { default as express } from 'express'; +import type * as db from '../db'; +import { isLoggedIn } from '../middleware/isLoggedIn'; +import { joinGroupsHandler } from '../handlers/usersToGroups'; + +const router = express.Router(); + +export function usersToGroupsRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { + router.post('/', isLoggedIn(dbPool), joinGroupsHandler(dbPool)); + return router; +} diff --git a/src/services/groupCategories.spec.ts b/src/services/groupCategories.spec.ts new file mode 100644 index 00000000..0486e49b --- /dev/null +++ b/src/services/groupCategories.spec.ts @@ -0,0 +1,85 @@ +import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import * as db from '../db'; +import { createDbPool } from '../utils/db/createDbPool'; +import { runMigrations } from '../utils/db/runMigrations'; +import { cleanup, seed } from '../utils/db/seed'; +import { canCreateGroupInGroupCategory, canViewGroupsInGroupCategory } from './groupCategories'; +import { eq } from 'drizzle-orm'; + +const DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432'; + +describe('service: groupCategories', () => { + let dbPool: PostgresJsDatabase; + let dbConnection: postgres.Sql>; + let groupCategory: db.GroupCategory | undefined; + + beforeAll(async () => { + const initDb = createDbPool(DB_CONNECTION_URL, { max: 1 }); + await runMigrations(DB_CONNECTION_URL); + dbPool = initDb.dbPool; + dbConnection = initDb.connection; + // seed + const { groupCategories } = await seed(dbPool); + + groupCategory = groupCategories[0]; + }); + + describe('check if user can create group in category:', function () { + test('default:', async function () { + if (!groupCategory) { + throw new Error('Group category not found'); + } + + const canCreate = await canCreateGroupInGroupCategory(dbPool, groupCategory.id); + + expect(canCreate).toBe(false); + }); + + test('userCanCreate: true', async function () { + if (!groupCategory) { + throw new Error('Group category not found'); + } + + await dbPool + .update(db.groupCategories) + .set({ userCanCreate: true }) + .where(eq(db.groupCategories.id, groupCategory.id)); + + const canCreate = await canCreateGroupInGroupCategory(dbPool, groupCategory.id); + + expect(canCreate).toBe(true); + }); + }); + + describe('check if user can view group category', function () { + test('default:', async function () { + if (!groupCategory) { + throw new Error('Group category not found'); + } + + const canView = await canViewGroupsInGroupCategory(dbPool, groupCategory.id); + + expect(canView).toBe(false); + }); + test('userCanView: true', async function () { + if (!groupCategory) { + throw new Error('Group category not found'); + } + + await dbPool + .update(db.groupCategories) + .set({ userCanView: true }) + .where(eq(db.groupCategories.id, groupCategory.id)); + + const canView = await canViewGroupsInGroupCategory(dbPool, groupCategory.id); + + expect(canView).toBe(true); + }); + }); + + afterAll(async () => { + await cleanup(dbPool); + await dbConnection.end(); + }); +}); diff --git a/src/services/groupCategories.ts b/src/services/groupCategories.ts new file mode 100644 index 00000000..39b3a60f --- /dev/null +++ b/src/services/groupCategories.ts @@ -0,0 +1,32 @@ +import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import * as db from '../db'; + +export async function canCreateGroupInGroupCategory( + dbPool: PostgresJsDatabase, + groupCategoryId: string, +) { + const groupCategory = await dbPool.query.groupCategories.findFirst({ + where: (fields, { eq }) => eq(fields.id, groupCategoryId), + }); + + if (!groupCategory) { + return false; + } + + return groupCategory.userCanCreate; +} + +export async function canViewGroupsInGroupCategory( + dbPool: PostgresJsDatabase, + groupCategoryId: string, +) { + const groupCategory = await dbPool.query.groupCategories.findFirst({ + where: (fields, { eq }) => eq(fields.id, groupCategoryId), + }); + + if (!groupCategory) { + return false; + } + + return groupCategory.userCanView; +} diff --git a/src/services/groups.spec.ts b/src/services/groups.spec.ts new file mode 100644 index 00000000..49fca9b1 --- /dev/null +++ b/src/services/groups.spec.ts @@ -0,0 +1,65 @@ +import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import * as db from '../db'; +import { createDbPool } from '../utils/db/createDbPool'; +import { runMigrations } from '../utils/db/runMigrations'; +import { cleanup } from '../utils/db/seed'; +import { createSecretGroup, generateSecret, getSecretGroup } from './groups'; + +const DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432'; + +describe('service: groups', () => { + let dbPool: PostgresJsDatabase; + let dbConnection: postgres.Sql>; + + beforeAll(async () => { + const initDb = createDbPool(DB_CONNECTION_URL, { max: 1 }); + await runMigrations(DB_CONNECTION_URL); + dbPool = initDb.dbPool; + dbConnection = initDb.connection; + }); + + test('generate secret:', async function () { + const secret = generateSecret(); + expect(secret).toHaveLength(12); + }); + + test('generate multiple secrets:', async function () { + const secrets = Array.from({ length: 10 }, () => generateSecret()); + + expect(secrets).toHaveLength(10); + expect(secrets).toEqual(expect.arrayContaining(secrets)); + // none should be the same + expect(new Set(secrets).size).toBe(secrets.length); + }); + + test('create a group:', async function () { + const rows = await createSecretGroup(dbPool, { + name: 'Test Group', + description: 'Test Description', + }); + + expect(rows).toHaveLength(1); + expect(rows[0]?.name).toBe('Test Group'); + expect(rows[0]?.description).toBe('Test Description'); + // secret should be generated + expect(rows[0]?.secret).toHaveLength(12); + }); + + test('get a group:', async function () { + const rows = await createSecretGroup(dbPool, { + name: 'Test Group', + description: 'Test Description', + }); + + const group = await getSecretGroup(dbPool, rows[0]?.secret ?? ''); + + expect(group?.name).toBe('Test Group'); + expect(group?.description).toBe('Test Description'); + }); + + afterAll(async () => { + await cleanup(dbPool); + await dbConnection.end(); + }); +}); diff --git a/src/services/groups.ts b/src/services/groups.ts index af813271..7dc52ff5 100644 --- a/src/services/groups.ts +++ b/src/services/groups.ts @@ -1,70 +1,34 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import * as db from '../db'; -import type { Request, Response } from 'express'; -import { eq } from 'drizzle-orm'; +import { z } from 'zod'; +import { insertGroupsSchema } from '../types/groups'; +import { randomBytes } from 'crypto'; -/** - * Retrieves groups by a specified group Category ID. - * @param dbPool The database connection pool. - * @returns An asynchronous function that handles the HTTP request and response. - */ -export function getGroupsByCategoryId(dbPool: PostgresJsDatabase) { - return async function (req: Request, res: Response) { - const groupCategoryId = req.params.groupCategoryId; - if (!groupCategoryId) { - return res.status(400).json({ error: 'groupCategoryId parameter is missing' }); - } - try { - const groupByCategoryId = await dbPool.query.groups.findMany({ - where: eq(db.groups.groupCategoryId, groupCategoryId), - }); - return res.json({ data: groupByCategoryId }); - } catch (e) { - console.error('error getting groups by Category id ' + JSON.stringify(e)); - return res.status(500).json({ error: 'internal server error' }); - } - }; -} - -/** - * Retrieves groups associated with a specific user filtered by a group Category ID. - * @param dbPool The database connection pool. - * @returns An asynchronous function that handles the HTTP request and response. - */ -export function getGroupsPerUserByCategoryId(dbPool: PostgresJsDatabase) { - return async function (req: Request, res: Response) { - const paramsUserId = req.params.userId; - const userId = req.session.userId; - const groupCategoryId = req.params.groupCategoryId; +export function createSecretGroup( + dbPool: PostgresJsDatabase, + body: z.infer, +) { + const secret = generateSecret(); - if (paramsUserId !== userId) { - return res.status(403).json({ errors: ['forbidden'] }); - } - if (!groupCategoryId) { - return res.status(400).json({ error: 'groupCategoryId parameter is missing' }); - } + const rows = dbPool + .insert(db.groups) + .values({ + ...body, + secret, + }) + .returning(); - try { - // Fetch all groups associated with the user - const userGroups = await dbPool.query.usersToGroups.findMany({ - with: { - group: true, - }, - where: eq(db.usersToGroups.userId, userId), - }); + return rows; +} - // Filter groups by groupCategoryId - const groupsWithCategoryId = userGroups.filter( - (group) => group.group.groupCategoryId === groupCategoryId, - ); +export function getSecretGroup(dbPool: PostgresJsDatabase, secret: string) { + const group = dbPool.query.groups.findFirst({ + where: (fields, { eq }) => eq(fields.secret, secret), + }); - // Extract the group objects - const out = groupsWithCategoryId.map((r) => r.group); + return group; +} - return res.json({ data: out }); - } catch (e) { - console.log('error getting groups per user by Category id ' + JSON.stringify(e)); - return res.status(500).json({ error: 'internal server error' }); - } - }; +export function generateSecret(): string { + return randomBytes(6).toString('hex'); } diff --git a/src/types/groups.ts b/src/types/groups.ts new file mode 100644 index 00000000..4e897fa0 --- /dev/null +++ b/src/types/groups.ts @@ -0,0 +1,12 @@ +import { createInsertSchema } from 'drizzle-zod'; +import { groups } from '../db'; +import { z } from 'zod'; + +export const insertGroupsSchema = createInsertSchema(groups, { + groupCategoryId: z.string().trim().min(1), +}).omit({ + createdAt: true, + updatedAt: true, + secret: true, + id: true, +}); diff --git a/src/types/usersToGroups.ts b/src/types/usersToGroups.ts new file mode 100644 index 00000000..d344cd1d --- /dev/null +++ b/src/types/usersToGroups.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +export const joinGroupsSchema = z.object({ + secret: z.string().min(1), +}); From 367024c012d8af21830524cc3441659d62f46a43 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Fri, 12 Apr 2024 11:36:50 -0500 Subject: [PATCH 10/21] Fix registrations creating new groups (#318) --- src/handlers/groupCategories.ts | 22 +++++++++++++++------- src/handlers/groups.ts | 6 ++++-- src/routers/groupCategories.ts | 2 +- src/utils/db/insertCustomGroups.ts | 17 ----------------- src/utils/db/seed.ts | 3 +++ 5 files changed, 23 insertions(+), 27 deletions(-) delete mode 100644 src/utils/db/insertCustomGroups.ts diff --git a/src/handlers/groupCategories.ts b/src/handlers/groupCategories.ts index be63a244..8a5e0aa3 100644 --- a/src/handlers/groupCategories.ts +++ b/src/handlers/groupCategories.ts @@ -1,7 +1,7 @@ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import type { Request, Response } from 'express'; import * as db from '../db'; -import { eq } from 'drizzle-orm'; +import { and, eq } from 'drizzle-orm'; import { canViewGroupsInGroupCategory } from '../services/groupCategories'; export function getGroupCategoriesHandler(dbPool: PostgresJsDatabase) { @@ -20,7 +20,7 @@ export function getGroupCategoryHandler(dbPool: PostgresJsDatabase) { } const groupCategory = await dbPool.query.groupCategories.findFirst({ - where: eq(db.groupCategories.id, groupCategoryId), + where: and(eq(db.groupCategories.id, groupCategoryId)), }); return res.json({ data: groupCategory }); @@ -29,13 +29,21 @@ export function getGroupCategoryHandler(dbPool: PostgresJsDatabase) { export function getGroupCategoriesGroupsHandler(dbPool: PostgresJsDatabase) { return async function (req: Request, res: Response) { - const groupCategoryId = req.params.id; + const groupCategoryName = req.params.name; - if (!groupCategoryId) { - return res.status(400).json({ error: 'Group Category ID is required' }); + if (!groupCategoryName) { + return res.status(400).json({ error: 'Group Category Name is required' }); + } + + const groupCategory = await dbPool.query.groupCategories.findFirst({ + where: eq(db.groupCategories.name, groupCategoryName), + }); + + if (!groupCategory) { + return res.status(404).json({ error: 'Group Category not found' }); } - const canView = await canViewGroupsInGroupCategory(dbPool, groupCategoryId); + const canView = await canViewGroupsInGroupCategory(dbPool, groupCategory.id); if (!canView) { return res @@ -44,7 +52,7 @@ export function getGroupCategoriesGroupsHandler(dbPool: PostgresJsDatabase) { return async function (req: Request, res: Response) { - const groups = await dbPool.query.groups.findMany(); + const groups = await dbPool.query.groups.findMany({ + where: isNull(db.groups.groupCategoryId), + }); return res.json({ data: groups }); }; } diff --git a/src/routers/groupCategories.ts b/src/routers/groupCategories.ts index 201a6c63..71422b98 100644 --- a/src/routers/groupCategories.ts +++ b/src/routers/groupCategories.ts @@ -13,7 +13,7 @@ const router = express.Router(); export function groupCategoriesRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { router.get('/', isLoggedIn(dbPool), getGroupCategoriesHandler(dbPool)); router.get('/:id', isLoggedIn(dbPool), getGroupCategoryHandler(dbPool)); - router.get('/:id/groups', isLoggedIn(dbPool), getGroupCategoriesGroupsHandler(dbPool)); + router.get('/:name/groups', isLoggedIn(dbPool), getGroupCategoriesGroupsHandler(dbPool)); return router; } diff --git a/src/utils/db/insertCustomGroups.ts b/src/utils/db/insertCustomGroups.ts deleted file mode 100644 index 11375549..00000000 --- a/src/utils/db/insertCustomGroups.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import * as db from '../../db'; - -/** - * Inserts groups into the 'groups' table based on data from a CSV file, after - * first deleting all existing data from the table. - * @param {PostgresJsDatabase} dbPool - The database connection pool. - * @param {string[]} groups - a list of groups to insert into the database. - * @returns {Promise} A promise that resolves when the insertion is complete. - * @throws {Error} If there is an error during file reading, database operations, or deletion of existing data. - */ -async function insertCustomGroups(dbPool: PostgresJsDatabase, groups: string[]) { - await dbPool.delete(db.groups); - await dbPool.insert(db.groups).values(groups.map((name) => ({ name }))); -} - -export { insertCustomGroups }; diff --git a/src/utils/db/seed.ts b/src/utils/db/seed.ts index c4e90188..5c4e2d2d 100644 --- a/src/utils/db/seed.ts +++ b/src/utils/db/seed.ts @@ -196,6 +196,9 @@ async function createGroups( { name: randCompanyName(), }, + { + name: randCompanyName(), + }, ]) .returning(); } From 627de3eea5beeeae5d6d9331524cd4d8da869ba1 Mon Sep 17 00:00:00 2001 From: diegoalzate Date: Fri, 12 Apr 2024 21:29:06 +0000 Subject: [PATCH 11/21] Bump to version 2.5.0 prerelease --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 77122ab6..c2f73c65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "forum", - "version": "2.4.0", + "version": "2.5.0", "description": "", "main": "dist/index.js", "scripts": { From e1915ec8c0c3d3d490c1b3eda84069e4b7837a7e Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Mon, 15 Apr 2024 14:17:32 -0500 Subject: [PATCH 12/21] Diego/hide fields on group registration (#323) * remove secret from get category groups endpoint * fix typo on group categories router * add migration to show fields on group registration --- migrations/0017_jazzy_revanche.sql | 1 + migrations/meta/0017_snapshot.json | 1562 ++++++++++++++++++++++++++++ migrations/meta/_journal.json | 7 + src/db/registrationFields.ts | 1 + src/handlers/groupCategories.ts | 8 + src/routers/api.ts | 3 +- 6 files changed, 1581 insertions(+), 1 deletion(-) create mode 100644 migrations/0017_jazzy_revanche.sql create mode 100644 migrations/meta/0017_snapshot.json diff --git a/migrations/0017_jazzy_revanche.sql b/migrations/0017_jazzy_revanche.sql new file mode 100644 index 00000000..c53a96cb --- /dev/null +++ b/migrations/0017_jazzy_revanche.sql @@ -0,0 +1 @@ +ALTER TABLE "registration_fields" ADD COLUMN "display_on_group_registration" boolean DEFAULT false; \ No newline at end of file diff --git a/migrations/meta/0017_snapshot.json b/migrations/meta/0017_snapshot.json new file mode 100644 index 00000000..91aeb18f --- /dev/null +++ b/migrations/meta/0017_snapshot.json @@ -0,0 +1,1562 @@ +{ + "id": "0d20d2de-c589-4e94-a0b1-fbd54238e209", + "prevId": "3bca2de9-9a2c-46ce-87c8-226969d43ec7", + "version": "5", + "dialect": "pg", + "tables": { + "comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_option_id": { + "name": "question_option_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comments_question_option_id_question_options_id_fk": { + "name": "comments_question_option_id_question_options_id_fk", + "tableFrom": "comments", + "tableTo": "question_options", + "columnsFrom": [ + "question_option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "cycles": { + "name": "cycles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'UPCOMING'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cycles_event_id_events_id_fk": { + "name": "cycles_event_id_events_id_fk", + "tableFrom": "cycles", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "registration_description": { + "name": "registration_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_display_rank": { + "name": "event_display_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "federated_credentials": { + "name": "federated_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federated_credentials_user_id_users_id_fk": { + "name": "federated_credentials_user_id_users_id_fk", + "tableFrom": "federated_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_subject_idx": { + "name": "provider_subject_idx", + "nullsNotDistinct": false, + "columns": [ + "provider", + "subject" + ] + } + } + }, + "forum_questions": { + "name": "forum_questions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_title": { + "name": "question_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "question_sub_title": { + "name": "question_sub_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "forum_questions_cycle_id_cycles_id_fk": { + "name": "forum_questions_cycle_id_cycles_id_fk", + "tableFrom": "forum_questions", + "tableTo": "cycles", + "columnsFrom": [ + "cycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "group_categories": { + "name": "group_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_can_create": { + "name": "user_can_create", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_can_view": { + "name": "user_can_view", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "group_categories_event_id_events_id_fk": { + "name": "group_categories_event_id_events_id_fk", + "tableFrom": "group_categories", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "groups": { + "name": "groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "groups_group_category_id_group_categories_id_fk": { + "name": "groups_group_category_id_group_categories_id_fk", + "tableFrom": "groups", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "groups_secret_unique": { + "name": "groups_secret_unique", + "nullsNotDistinct": false, + "columns": [ + "secret" + ] + } + } + }, + "users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram": { + "name": "telegram", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_telegram_unique": { + "name": "users_telegram_unique", + "nullsNotDistinct": false, + "columns": [ + "telegram" + ] + } + } + }, + "registrations": { + "name": "registrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'DRAFT'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registrations_user_id_users_id_fk": { + "name": "registrations_user_id_users_id_fk", + "tableFrom": "registrations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registrations_event_id_events_id_fk": { + "name": "registrations_event_id_events_id_fk", + "tableFrom": "registrations", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registrations_group_id_groups_id_fk": { + "name": "registrations_group_id_groups_id_fk", + "tableFrom": "registrations", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_field_options": { + "name": "registration_field_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_field_id": { + "name": "registration_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_field_options_registration_field_id_registration_fields_id_fk": { + "name": "registration_field_options_registration_field_id_registration_fields_id_fk", + "tableFrom": "registration_field_options", + "tableTo": "registration_fields", + "columnsFrom": [ + "registration_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "question_options": { + "name": "question_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "registration_id": { + "name": "registration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_title": { + "name": "option_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "option_sub_title": { + "name": "option_sub_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "accepted": { + "name": "accepted", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vote_score": { + "name": "vote_score", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0.0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "question_options_user_id_users_id_fk": { + "name": "question_options_user_id_users_id_fk", + "tableFrom": "question_options", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "question_options_registration_id_registrations_id_fk": { + "name": "question_options_registration_id_registrations_id_fk", + "tableFrom": "question_options", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "question_options_question_id_forum_questions_id_fk": { + "name": "question_options_question_id_forum_questions_id_fk", + "tableFrom": "question_options", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "votes": { + "name": "votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_id": { + "name": "option_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "num_of_votes": { + "name": "num_of_votes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "votes_user_id_users_id_fk": { + "name": "votes_user_id_users_id_fk", + "tableFrom": "votes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_option_id_question_options_id_fk": { + "name": "votes_option_id_question_options_id_fk", + "tableFrom": "votes", + "tableTo": "question_options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_question_id_forum_questions_id_fk": { + "name": "votes_question_id_forum_questions_id_fk", + "tableFrom": "votes", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_fields": { + "name": "registration_fields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'TEXT'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_option_type": { + "name": "question_option_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "fields_display_rank": { + "name": "fields_display_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "character_limit": { + "name": "character_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "display_on_group_registration": { + "name": "display_on_group_registration", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_fields_event_id_events_id_fk": { + "name": "registration_fields_event_id_events_id_fk", + "tableFrom": "registration_fields", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registration_fields_question_id_forum_questions_id_fk": { + "name": "registration_fields_question_id_forum_questions_id_fk", + "tableFrom": "registration_fields", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_data": { + "name": "registration_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "registration_field_id": { + "name": "registration_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_data_registration_id_registrations_id_fk": { + "name": "registration_data_registration_id_registrations_id_fk", + "tableFrom": "registration_data", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registration_data_registration_field_id_registration_fields_id_fk": { + "name": "registration_data_registration_field_id_registration_fields_id_fk", + "tableFrom": "registration_data", + "tableTo": "registration_fields", + "columnsFrom": [ + "registration_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users_to_groups": { + "name": "users_to_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_groups_user_id_users_id_fk": { + "name": "users_to_groups_user_id_users_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_groups_group_id_groups_id_fk": { + "name": "users_to_groups_group_id_groups_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_groups_group_category_id_group_categories_id_fk": { + "name": "users_to_groups_group_category_id_group_categories_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_attributes": { + "name": "user_attributes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attribute_key": { + "name": "attribute_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "attribute_value": { + "name": "attribute_value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_attributes_user_id_users_id_fk": { + "name": "user_attributes_user_id_users_id_fk", + "tableFrom": "user_attributes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "likes_user_id_users_id_fk": { + "name": "likes_user_id_users_id_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "likes_comment_id_comments_id_fk": { + "name": "likes_comment_id_comments_id_fk", + "tableFrom": "likes", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "notification_types": { + "name": "notification_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "value": { + "name": "value", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notification_types_value_unique": { + "name": "notification_types_value_unique", + "nullsNotDistinct": false, + "columns": [ + "value" + ] + } + } + }, + "users_to_notifications": { + "name": "users_to_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notification_type_id": { + "name": "notification_type_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_notifications_user_id_users_id_fk": { + "name": "users_to_notifications_user_id_users_id_fk", + "tableFrom": "users_to_notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_notifications_notification_type_id_notification_types_id_fk": { + "name": "users_to_notifications_notification_type_id_notification_types_id_fk", + "tableFrom": "users_to_notifications", + "tableTo": "notification_types", + "columnsFrom": [ + "notification_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "questions_to_group_categories": { + "name": "questions_to_group_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "questions_to_group_categories_question_id_forum_questions_id_fk": { + "name": "questions_to_group_categories_question_id_forum_questions_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "questions_to_group_categories_group_category_id_group_categories_id_fk": { + "name": "questions_to_group_categories_group_category_id_group_categories_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index b85575ec..8fda86f5 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1712848852970, "tag": "0016_ambitious_rictor", "breakpoints": true + }, + { + "idx": 17, + "version": "5", + "when": 1713192184507, + "tag": "0017_jazzy_revanche", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/registrationFields.ts b/src/db/registrationFields.ts index d38633cb..47855f5b 100644 --- a/src/db/registrationFields.ts +++ b/src/db/registrationFields.ts @@ -17,6 +17,7 @@ export const registrationFields = pgTable('registration_fields', { questionOptionType: varchar('question_option_type'), fieldDisplayRank: integer('fields_display_rank'), characterLimit: integer('character_limit').default(0), + displayOnGroupRegistration: boolean('display_on_group_registration').default(false), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); diff --git a/src/handlers/groupCategories.ts b/src/handlers/groupCategories.ts index 8a5e0aa3..fd806b33 100644 --- a/src/handlers/groupCategories.ts +++ b/src/handlers/groupCategories.ts @@ -53,6 +53,14 @@ export function getGroupCategoriesGroupsHandler(dbPool: PostgresJsDatabase Date: Mon, 15 Apr 2024 20:39:26 -0500 Subject: [PATCH 13/21] Support groups without secret (#326) * update join groups handler to support public groups * remove undefined category with seed * rename key from id to groupId in join group payload * update permissions on scripts * fix tests --- src/handlers/usersToGroups.ts | 22 +++++++++++ src/services/groupCategories.spec.ts | 7 +++- src/services/statistics.spec.ts | 2 +- src/services/usersToGroups.spec.ts | 17 --------- src/services/votes.spec.ts | 56 ++++++++++++++-------------- src/types/usersToGroups.ts | 8 +++- src/utils/db/seed.ts | 25 +++---------- 7 files changed, 70 insertions(+), 67 deletions(-) diff --git a/src/handlers/usersToGroups.ts b/src/handlers/usersToGroups.ts index 1c8cc73a..02ce05e7 100644 --- a/src/handlers/usersToGroups.ts +++ b/src/handlers/usersToGroups.ts @@ -4,17 +4,39 @@ import * as db from '../db'; import { joinGroupsSchema } from '../types/usersToGroups'; import { getSecretGroup } from '../services/groups'; import { upsertUsersToGroups } from '../services/usersToGroups'; +import { eq } from 'drizzle-orm'; export function joinGroupsHandler(dbPool: PostgresJsDatabase) { return async (req: Request, res: Response) => { const userId = req.session.userId; const body = joinGroupsSchema.safeParse(req.body); + // does not have secret nor id if (!body.success) { return res.status(400).json({ errors: body.error.errors }); } try { + // public group + if ('groupId' in body.data) { + const group = await dbPool.query.groups.findFirst({ + where: eq(db.groups.id, body.data.groupId), + }); + + if (!group) { + return res.status(404).json({ error: 'Group not found' }); + } + + if (group.secret) { + return res.status(400).json({ error: 'Group is secret' }); + } + + const userToGroup = await upsertUsersToGroups(dbPool, userId, [body.data.groupId]); + + return res.json({ data: userToGroup }); + } + + // secret group const secretGroup = await getSecretGroup(dbPool, body.data.secret); if (!secretGroup) { diff --git a/src/services/groupCategories.spec.ts b/src/services/groupCategories.spec.ts index 0486e49b..a74ad8aa 100644 --- a/src/services/groupCategories.spec.ts +++ b/src/services/groupCategories.spec.ts @@ -53,11 +53,16 @@ describe('service: groupCategories', () => { }); describe('check if user can view group category', function () { - test('default:', async function () { + test('userCanView: false', async function () { if (!groupCategory) { throw new Error('Group category not found'); } + await dbPool + .update(db.groupCategories) + .set({ userCanView: false }) + .where(eq(db.groupCategories.id, groupCategory.id)); + const canView = await canViewGroupsInGroupCategory(dbPool, groupCategory.id); expect(canView).toBe(false); diff --git a/src/services/statistics.spec.ts b/src/services/statistics.spec.ts index 3fdf6c94..bfbc5132 100644 --- a/src/services/statistics.spec.ts +++ b/src/services/statistics.spec.ts @@ -31,7 +31,7 @@ describe('service: statistics', () => { questionOption = questionOptions[0]; forumQuestion = forumQuestions[0]; user = users[0]; - otherUser = users[1]; + otherUser = users[2]; userTestData = { numOfVotes: 4, optionId: questionOption?.id ?? '', diff --git a/src/services/usersToGroups.spec.ts b/src/services/usersToGroups.spec.ts index 8a394081..d53c07a3 100644 --- a/src/services/usersToGroups.spec.ts +++ b/src/services/usersToGroups.spec.ts @@ -45,23 +45,6 @@ describe('service: usersToGroups', function () { expect(newUserGroup?.userId).toBe(newUser?.id); }); - test('can save initial groups when label is null', async function () { - // Get the newly inserted user - const newUser1 = await dbPool.query.users.findFirst({ - where: eq(db.users.username, 'NewUser1'), - }); - - await upsertUsersToGroups(dbPool, newUser1?.id ?? '', [defaultGroups[3]?.id ?? '']); - - // Find the userToGroup relationship for the newUser and the chosen group - const newUserGroup = await dbPool.query.usersToGroups.findFirst({ - where: eq(db.usersToGroups.userId, newUser1?.id ?? ''), - }); - - expect(newUserGroup).toBeDefined(); - expect(newUserGroup?.userId).toBe(newUser1?.id); - }); - test('can overwrite old user groups', async function () { await upsertUsersToGroups(dbPool, user?.id ?? '', [defaultGroups[2]?.id ?? '']); const group = await dbPool.query.usersToGroups.findFirst({ diff --git a/src/services/votes.spec.ts b/src/services/votes.spec.ts index 48dbd9a5..0d158ae7 100644 --- a/src/services/votes.spec.ts +++ b/src/services/votes.spec.ts @@ -215,19 +215,6 @@ describe('service: votes', () => { const votesDictionary = await numOfVotesDictionary(voteArray); const groups = await groupsDictionary(dbPool, votesDictionary, [groupCategory!.id]); - expect(groups).toBeDefined(); - expect(groups['unexpectedKey']).toBeUndefined(); - expect(typeof groups).toBe('object'); - expect(Object.keys(groups).length).toEqual(2); - expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); - }); - - test('only return baseline groups for users who voted for the option as non of the users is in the additional group category', async () => { - // Get vote data required for groups - const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); - const votesDictionary = await numOfVotesDictionary(voteArray); - const groups = await groupsDictionary(dbPool, votesDictionary, [otherGroupCategory!.id]); - expect(groups).toBeDefined(); expect(groups['unexpectedKey']).toBeUndefined(); expect(typeof groups).toBe('object'); @@ -235,21 +222,34 @@ describe('service: votes', () => { expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); }); - test('only return baseline groups when no addtional group category gets provided', async () => { - // Get vote data required for groups - const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); - const votesDictionary = await numOfVotesDictionary(voteArray); - - const groups = await groupsDictionary(dbPool, votesDictionary, [ - '00000000-0000-0000-0000-000000000000', - ]); - - expect(groups).toBeDefined(); - expect(groups['unexpectedKey']).toBeUndefined(); - expect(typeof groups).toBe('object'); - expect(Object.keys(groups).length).toEqual(1); - expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); - }); + // test('only return baseline groups for users who voted for the option as non of the users is in the additional group category', async () => { + // // Get vote data required for groups + // const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); + // const votesDictionary = await numOfVotesDictionary(voteArray); + // const groups = await groupsDictionary(dbPool, votesDictionary, [otherGroupCategory!.id]); + + // expect(groups).toBeDefined(); + // expect(groups['unexpectedKey']).toBeUndefined(); + // expect(typeof groups).toBe('object'); + // expect(Object.keys(groups).length).toEqual(1); + // expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); + // }); + + // test('only return baseline groups when no addtional group category gets provided', async () => { + // // Get vote data required for groups + // const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); + // const votesDictionary = await numOfVotesDictionary(voteArray); + + // const groups = await groupsDictionary(dbPool, votesDictionary, [ + // '00000000-0000-0000-0000-000000000000', + // ]); + + // expect(groups).toBeDefined(); + // expect(groups['unexpectedKey']).toBeUndefined(); + // expect(typeof groups).toBe('object'); + // expect(Object.keys(groups).length).toEqual(1); + // expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); + // }); test('should calculate the plural score correctly', () => { // Mock groups dictionary diff --git a/src/types/usersToGroups.ts b/src/types/usersToGroups.ts index d344cd1d..146c789f 100644 --- a/src/types/usersToGroups.ts +++ b/src/types/usersToGroups.ts @@ -1,5 +1,11 @@ import { z } from 'zod'; -export const joinGroupsSchema = z.object({ +export const joinSecretGroupsSchema = z.object({ secret: z.string().min(1), }); + +export const joinPublicGroupsSchema = z.object({ + groupId: z.string().min(1), +}); + +export const joinGroupsSchema = z.union([joinSecretGroupsSchema, joinPublicGroupsSchema]); diff --git a/src/utils/db/seed.ts b/src/utils/db/seed.ts index 5c4e2d2d..9222055b 100644 --- a/src/utils/db/seed.ts +++ b/src/utils/db/seed.ts @@ -15,7 +15,7 @@ async function seed(dbPool: PostgresJsDatabase) { dbPool, users.map((u) => u.id!), groups.map((g) => g.id!), - groupCategories[0]?.id, + groupCategories[0]!.id, ); const questionsToGroupCategories = await createQuestionsToGroupCategories( dbPool, @@ -162,12 +162,14 @@ async function createGroupCategories(dbPool: PostgresJsDatabase, even .insert(db.groupCategories) .values([ { - name: 'Category A', + name: 'affiliation', eventId: eventId, + userCanView: true, }, { - name: 'Category B', + name: 'secrets', eventId: eventId, + userCanCreate: true, }, ]) .returning(); @@ -193,12 +195,6 @@ async function createGroups( name: randCompanyName(), groupCategoryId: groupIdTwo, }, - { - name: randCompanyName(), - }, - { - name: randCompanyName(), - }, ]) .returning(); } @@ -215,7 +211,7 @@ async function createUsersToGroups( dbPool: PostgresJsDatabase, userIds: string[], groupIds: string[], - groupCategoryId: string | undefined, + groupCategoryId: string, ) { // assign users to groups const usersToGroups = userIds.map((userId, index) => ({ @@ -224,15 +220,6 @@ async function createUsersToGroups( groupCategoryId, })); - // Add baseline group for each user (i.e. each user must be assigned to at least one group at all times) - userIds.forEach((userId) => { - usersToGroups.push({ - userId, - groupId: groupIds[3]!, - groupCategoryId: undefined, // udefined because currently affiliation does not have a group category id - }); - }); - return dbPool.insert(db.usersToGroups).values(usersToGroups).returning(); } From fe53fda0cb3a4ae900107b887279c2efd87571c6 Mon Sep 17 00:00:00 2001 From: Martin Benedikt Busch <43137759+MartinBenediktBusch@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:33:15 +0200 Subject: [PATCH 14/21] Remove the handling of null values (#324) * remove null values from usersToGroups * fix seed and tests * update seed * fix vote service tests * remove null from group handler --- src/handlers/groups.ts | 15 +----- src/services/statistics.spec.ts | 2 +- src/services/usersToGroups.spec.ts | 46 +++++++++++++++--- src/services/usersToGroups.ts | 77 ++++++++++-------------------- src/services/votes.spec.ts | 68 +++++++++++++------------- src/services/votes.ts | 14 ++---- src/utils/db/seed.ts | 49 +++++++++++++++---- 7 files changed, 146 insertions(+), 125 deletions(-) diff --git a/src/handlers/groups.ts b/src/handlers/groups.ts index 52ce64a0..0cc79a9e 100644 --- a/src/handlers/groups.ts +++ b/src/handlers/groups.ts @@ -1,24 +1,11 @@ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import type { Request, Response } from 'express'; import * as db from '../db'; -import { eq, isNull } from 'drizzle-orm'; +import { eq } from 'drizzle-orm'; import { insertGroupsSchema } from '../types/groups'; import { canCreateGroupInGroupCategory } from '../services/groupCategories'; import { createSecretGroup } from '../services/groups'; import { upsertUsersToGroups } from '../services/usersToGroups'; -/** - * Retrieves all groups from the database. - * @param dbPool The database connection pool. - * @returns An asynchronous function that handles the HTTP request and response. - */ -export function getGroupsHandler(dbPool: PostgresJsDatabase) { - return async function (req: Request, res: Response) { - const groups = await dbPool.query.groups.findMany({ - where: isNull(db.groups.groupCategoryId), - }); - return res.json({ data: groups }); - }; -} export function getGroupRegistrationsHandler(dbPool: PostgresJsDatabase) { return async function (req: Request, res: Response) { diff --git a/src/services/statistics.spec.ts b/src/services/statistics.spec.ts index bfbc5132..3fdf6c94 100644 --- a/src/services/statistics.spec.ts +++ b/src/services/statistics.spec.ts @@ -31,7 +31,7 @@ describe('service: statistics', () => { questionOption = questionOptions[0]; forumQuestion = forumQuestions[0]; user = users[0]; - otherUser = users[2]; + otherUser = users[1]; userTestData = { numOfVotes: 4, optionId: questionOption?.id ?? '', diff --git a/src/services/usersToGroups.spec.ts b/src/services/usersToGroups.spec.ts index d53c07a3..0d38b648 100644 --- a/src/services/usersToGroups.spec.ts +++ b/src/services/usersToGroups.spec.ts @@ -1,7 +1,7 @@ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import * as db from '../db'; import { upsertUsersToGroups } from './usersToGroups'; -import { eq } from 'drizzle-orm'; +import { eq, and } from 'drizzle-orm'; import { createDbPool } from '../utils/db/createDbPool'; import postgres from 'postgres'; import { runMigrations } from '../utils/db/runMigrations'; @@ -25,7 +25,6 @@ describe('service: usersToGroups', function () { defaultGroups = groups; // insert users without group assignment await dbPool.insert(db.users).values({ username: 'NewUser', email: 'SomeEmail' }); - await dbPool.insert(db.users).values({ username: 'NewUser1', email: 'SomeEmail1' }); }); test('can save initial groups', async function () { @@ -45,13 +44,46 @@ describe('service: usersToGroups', function () { expect(newUserGroup?.userId).toBe(newUser?.id); }); + test('can save another group for the same user with a different category id', async function () { + // Get the newly inserted user + const newUser = await dbPool.query.users.findFirst({ + where: eq(db.users.username, 'NewUser'), + }); + + await upsertUsersToGroups(dbPool, newUser?.id ?? '', [defaultGroups[2]?.id ?? '']); + + // Find the userToGroup relationship for the newUser and the chosen group + const newUserGroup = await dbPool.query.usersToGroups.findFirst({ + where: and( + eq(db.usersToGroups.userId, newUser?.id ?? ''), + eq(db.usersToGroups.groupId, defaultGroups[2]?.id ?? ''), + ), + }); + + expect(newUserGroup).toBeDefined(); + expect(newUserGroup?.userId).toBe(newUser?.id); + expect(newUserGroup?.groupId).toBe(defaultGroups[2]?.id); + }); + test('can overwrite old user groups', async function () { - await upsertUsersToGroups(dbPool, user?.id ?? '', [defaultGroups[2]?.id ?? '']); - const group = await dbPool.query.usersToGroups.findFirst({ - where: eq(db.usersToGroups.groupId, defaultGroups[2]?.id ?? ''), + const newUser = await dbPool.query.users.findFirst({ + where: eq(db.users.username, 'NewUser'), + }); + + await upsertUsersToGroups(dbPool, newUser?.id ?? '', [defaultGroups[1]?.id ?? '']); + + // Find the userToGroup relationship for the newUser and the chosen group + const newUserGroup = await dbPool.query.usersToGroups.findFirst({ + where: and( + eq(db.usersToGroups.userId, newUser?.id ?? ''), + eq(db.usersToGroups.groupId, defaultGroups[1]?.id ?? ''), + ), }); - expect(group?.userId).toBeDefined; - expect(group?.userId).toBe(user?.id); + + expect(newUserGroup).toBeDefined(); + expect(newUserGroup?.userId).toBe(newUser?.id); + expect(newUserGroup?.groupId).toBe(defaultGroups[1]?.id); + expect(newUserGroup?.groupId).not.toBe(defaultGroups[2]?.id); }); test('handles non-existent group IDs', async function () { diff --git a/src/services/usersToGroups.ts b/src/services/usersToGroups.ts index 15b97e56..566a632c 100644 --- a/src/services/usersToGroups.ts +++ b/src/services/usersToGroups.ts @@ -1,6 +1,6 @@ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import * as db from '../db'; -import { eq, and, isNull } from 'drizzle-orm'; +import { eq, and } from 'drizzle-orm'; /** * Upserts the user-to-groups associations in the database for a given user. @@ -26,34 +26,32 @@ export async function upsertUsersToGroups( continue; } - const groupCategoryId = group.groupCategoryId ?? null; + // get group category id + const groupCategoryId = group.groupCategoryId; - if (groupCategoryId === null) { - await overwriteUsersToGroups(dbPool, userId, groupId); - } else { - const existingAssociation = await dbPool.query.usersToGroups.findFirst({ - where: and( - eq(db.usersToGroups.userId, userId), - eq(db.usersToGroups.groupCategoryId, groupCategoryId!), - ), - }); + // get all groups associated with a user by group category + const existingAssociation = await dbPool.query.usersToGroups.findFirst({ + where: and( + eq(db.usersToGroups.userId, userId), + eq(db.usersToGroups.groupCategoryId, groupCategoryId!), + ), + }); - if (existingAssociation) { - await dbPool - .update(db.usersToGroups) - .set({ userId, groupId, groupCategoryId, updatedAt: new Date() }) - .where( - and( - eq(db.usersToGroups.userId, userId), - eq(db.usersToGroups.groupCategoryId, groupCategoryId!), - ), - ); - } else { - await dbPool - .insert(db.usersToGroups) - .values({ userId, groupId, groupCategoryId }) - .returning(); - } + if (existingAssociation) { + await dbPool + .update(db.usersToGroups) + .set({ userId, groupId, groupCategoryId, updatedAt: new Date() }) + .where( + and( + eq(db.usersToGroups.userId, userId), + eq(db.usersToGroups.groupCategoryId, groupCategoryId!), + ), + ); + } else { + await dbPool + .insert(db.usersToGroups) + .values({ userId, groupId, groupCategoryId }) + .returning(); } } @@ -66,28 +64,3 @@ export async function upsertUsersToGroups( return null; } } - -// Handle cases where Category ID is zero. This function and its references will be deleted once we require -// group Category ids to be mandatory in the group table. -export async function overwriteUsersToGroups( - dbPool: PostgresJsDatabase, - userId: string, - newGroupId: string, -): Promise { - // delete all groups with Category id zero that previously existed - try { - await dbPool - .delete(db.usersToGroups) - .where(and(eq(db.usersToGroups.userId, userId), isNull(db.usersToGroups.groupCategoryId))); - } catch (e) { - console.log('error deleting user groups ' + JSON.stringify(e)); - return null; - } - // save the new ones - const newUsersToGroups = await dbPool - .insert(db.usersToGroups) - .values({ userId, groupId: newGroupId }) - .returning(); - - return newUsersToGroups; -} diff --git a/src/services/votes.spec.ts b/src/services/votes.spec.ts index 0d158ae7..2246027f 100644 --- a/src/services/votes.spec.ts +++ b/src/services/votes.spec.ts @@ -33,6 +33,7 @@ describe('service: votes', () => { let otherForumQuestion: db.ForumQuestion | undefined; let groupCategory: db.GroupCategory | undefined; let otherGroupCategory: db.GroupCategory | undefined; + let unrelatedGroupCategory: db.GroupCategory | undefined; let user: db.User | undefined; let secondUser: db.User | undefined; let thirdUser: db.User | undefined; @@ -50,6 +51,7 @@ describe('service: votes', () => { otherForumQuestion = forumQuestions[1]; groupCategory = groupCategories[0]; otherGroupCategory = groupCategories[1]; + unrelatedGroupCategory = groupCategories[2]; user = users[0]; secondUser = users[1]; thirdUser = users[2]; @@ -200,17 +202,15 @@ describe('service: votes', () => { }); }); - test('that query group categories returns an empty array if their are no group categories for a specific question', async () => { - // Get vote data required for groups + test('that query group categories returns an empty array if their are no group categories specified for a specific question', async () => { const groupCategoriesIdArray = await queryGroupCategories(dbPool, otherForumQuestion!.id); expect(groupCategoriesIdArray).toBeDefined(); - expect(groupCategoriesIdArray.length).toBe(1); + expect(groupCategoriesIdArray.length).toBe(0); expect(Array.isArray(groupCategoriesIdArray)).toBe(true); - expect(groupCategoriesIdArray).toEqual(['00000000-0000-0000-0000-000000000000']); + expect(groupCategoriesIdArray).toEqual([]); }); test('only return groups for users who voted for the option', async () => { - // Get vote data required for groups const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); const votesDictionary = await numOfVotesDictionary(voteArray); const groups = await groupsDictionary(dbPool, votesDictionary, [groupCategory!.id]); @@ -222,34 +222,36 @@ describe('service: votes', () => { expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); }); - // test('only return baseline groups for users who voted for the option as non of the users is in the additional group category', async () => { - // // Get vote data required for groups - // const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); - // const votesDictionary = await numOfVotesDictionary(voteArray); - // const groups = await groupsDictionary(dbPool, votesDictionary, [otherGroupCategory!.id]); - - // expect(groups).toBeDefined(); - // expect(groups['unexpectedKey']).toBeUndefined(); - // expect(typeof groups).toBe('object'); - // expect(Object.keys(groups).length).toEqual(1); - // expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); - // }); - - // test('only return baseline groups when no addtional group category gets provided', async () => { - // // Get vote data required for groups - // const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); - // const votesDictionary = await numOfVotesDictionary(voteArray); - - // const groups = await groupsDictionary(dbPool, votesDictionary, [ - // '00000000-0000-0000-0000-000000000000', - // ]); - - // expect(groups).toBeDefined(); - // expect(groups['unexpectedKey']).toBeUndefined(); - // expect(typeof groups).toBe('object'); - // expect(Object.keys(groups).length).toEqual(1); - // expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); - // }); + test('only return groups for users who voted for the option with two elidgible group categories', async () => { + const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); + const votesDictionary = await numOfVotesDictionary(voteArray); + const groups = await groupsDictionary(dbPool, votesDictionary, [ + groupCategory!.id, + otherGroupCategory!.id, + ]); + + expect(groups).toBeDefined(); + expect(groups['unexpectedKey']).toBeUndefined(); + expect(typeof groups).toBe('object'); + expect(Object.keys(groups).length).toEqual(2); + expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); + }); + + test('only return baseline groups for users who voted for the option as non of the users is in the additional group category', async () => { + // Get vote data required for groups + const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); + const votesDictionary = await numOfVotesDictionary(voteArray); + const groups = await groupsDictionary(dbPool, votesDictionary, [ + groupCategory!.id, + unrelatedGroupCategory!.id, + ]); + + expect(groups).toBeDefined(); + expect(groups['unexpectedKey']).toBeUndefined(); + expect(typeof groups).toBe('object'); + expect(Object.keys(groups).length).toEqual(1); + expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); + }); test('should calculate the plural score correctly', () => { // Mock groups dictionary diff --git a/src/services/votes.ts b/src/services/votes.ts index 8dfb7329..10a148c9 100644 --- a/src/services/votes.ts +++ b/src/services/votes.ts @@ -89,14 +89,12 @@ export async function queryGroupCategories( .from(db.questionsToGroupCategories) .where(eq(db.questionsToGroupCategories.questionId, questionId)); - // Need to due this adjustment because currently group_category_id is nullable due to affiliations having no label. - const groupCategoryIds = groupCategories - .map((category) => category.groupCategoryId) - .filter((id) => id !== null) as string[]; + // Need to due this adjustment because currently groupCategoryId is nullable in the datatable definition. + const groupCategoryIds: string[] = groupCategories.map((category) => category.groupCategoryId!); - // Returning dummy uuid if there are no group categories found if (groupCategoryIds.length === 0) { - return ['00000000-0000-0000-0000-000000000000']; + console.error('Group Category ID is Missing'); + return []; } return groupCategoryIds; @@ -120,9 +118,7 @@ export async function groupsDictionary( WHERE user_id IN (${Object.keys(numOfVotesDictionary) .map((id) => `'${id}'`) .join(', ')}) - AND (group_category_id IN (${groupCategories - .map((category) => `'${category}'`) - .join(', ')}) OR group_category_id IS NULL) + AND group_category_id IN (${groupCategories.map((category) => `'${category}'`).join(', ')}) GROUP BY group_id `), ); diff --git a/src/utils/db/seed.ts b/src/utils/db/seed.ts index 9222055b..0746a35c 100644 --- a/src/utils/db/seed.ts +++ b/src/utils/db/seed.ts @@ -9,13 +9,19 @@ async function seed(dbPool: PostgresJsDatabase) { const forumQuestions = await createForumQuestions(dbPool, cycles[0]?.id); const questionOptions = await createQuestionOptions(dbPool, forumQuestions[0]?.id); const groupCategories = await createGroupCategories(dbPool, events[0]?.id); - const groups = await createGroups(dbPool, groupCategories[0]?.id, groupCategories[1]?.id); + const groups = await createGroups( + dbPool, + groupCategories[0]?.id, + groupCategories[1]?.id, + groupCategories[2]?.id, + ); const users = await createUsers(dbPool); const usersToGroups = await createUsersToGroups( dbPool, users.map((u) => u.id!), groups.map((g) => g.id!), groupCategories[0]!.id, + groupCategories[1]!.id, ); const questionsToGroupCategories = await createQuestionsToGroupCategories( dbPool, @@ -166,6 +172,16 @@ async function createGroupCategories(dbPool: PostgresJsDatabase, even eventId: eventId, userCanView: true, }, + { + name: 'category A', + eventId: eventId, + userCanView: true, + }, + { + name: 'category B', + eventId: eventId, + userCanView: true, + }, { name: 'secrets', eventId: eventId, @@ -177,23 +193,28 @@ async function createGroupCategories(dbPool: PostgresJsDatabase, even async function createGroups( dbPool: PostgresJsDatabase, - groupIdOne?: string, - groupIdTwo?: string, + baselineCategory?: string, + categoryOne?: string, + categoryTwo?: string, ) { return dbPool .insert(db.groups) .values([ { name: randCompanyName(), - groupCategoryId: groupIdOne, + groupCategoryId: baselineCategory, }, { name: randCompanyName(), - groupCategoryId: groupIdOne, + groupCategoryId: categoryOne, }, { name: randCompanyName(), - groupCategoryId: groupIdTwo, + groupCategoryId: categoryOne, + }, + { + name: randCompanyName(), + groupCategoryId: categoryTwo, }, ]) .returning(); @@ -211,15 +232,25 @@ async function createUsersToGroups( dbPool: PostgresJsDatabase, userIds: string[], groupIds: string[], - groupCategoryId: string, + baselineGroupCategory: string | undefined, + otherGroupCategory: string | undefined, ) { // assign users to groups const usersToGroups = userIds.map((userId, index) => ({ userId, - groupId: index < 2 ? groupIds[0]! : groupIds[1]!, - groupCategoryId, + groupId: index < 2 ? groupIds[1]! : groupIds[2]!, + groupCategoryId: otherGroupCategory, })); + // Add baseline group for each user (i.e. each user must be assigned to at least one group at all times) + userIds.forEach((userId) => { + usersToGroups.push({ + userId, + groupId: groupIds[0]!, + groupCategoryId: baselineGroupCategory, + }); + }); + return dbPool.insert(db.usersToGroups).values(usersToGroups).returning(); } From 799ff38349751d20e513658e280b12693fb0d265 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Tue, 16 Apr 2024 10:33:36 -0500 Subject: [PATCH 15/21] Add alerts (#328) * add alerts table * add router for alerts * add title, description and link to alerts --- migrations/0018_abandoned_mongoose.sql | 11 + migrations/meta/0018_snapshot.json | 1630 ++++++++++++++++++++++++ migrations/meta/_journal.json | 7 + src/db/alerts.ts | 15 + src/db/index.ts | 1 + src/handlers/alerts.ts | 22 + src/routers/alerts.ts | 11 + src/routers/api.ts | 2 + 8 files changed, 1699 insertions(+) create mode 100644 migrations/0018_abandoned_mongoose.sql create mode 100644 migrations/meta/0018_snapshot.json create mode 100644 src/db/alerts.ts create mode 100644 src/handlers/alerts.ts create mode 100644 src/routers/alerts.ts diff --git a/migrations/0018_abandoned_mongoose.sql b/migrations/0018_abandoned_mongoose.sql new file mode 100644 index 00000000..924b5f21 --- /dev/null +++ b/migrations/0018_abandoned_mongoose.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS "alerts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" varchar(256) NOT NULL, + "description" varchar(1024), + "link" varchar(256), + "start_at" timestamp, + "end_at" timestamp, + "active" boolean DEFAULT false, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); diff --git a/migrations/meta/0018_snapshot.json b/migrations/meta/0018_snapshot.json new file mode 100644 index 00000000..8ae39496 --- /dev/null +++ b/migrations/meta/0018_snapshot.json @@ -0,0 +1,1630 @@ +{ + "id": "9e82519c-4853-44bf-ac83-165a4e51c435", + "prevId": "0d20d2de-c589-4e94-a0b1-fbd54238e209", + "version": "5", + "dialect": "pg", + "tables": { + "alerts": { + "name": "alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_at": { + "name": "end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_option_id": { + "name": "question_option_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comments_question_option_id_question_options_id_fk": { + "name": "comments_question_option_id_question_options_id_fk", + "tableFrom": "comments", + "tableTo": "question_options", + "columnsFrom": [ + "question_option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "cycles": { + "name": "cycles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'UPCOMING'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cycles_event_id_events_id_fk": { + "name": "cycles_event_id_events_id_fk", + "tableFrom": "cycles", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "registration_description": { + "name": "registration_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_display_rank": { + "name": "event_display_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "federated_credentials": { + "name": "federated_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federated_credentials_user_id_users_id_fk": { + "name": "federated_credentials_user_id_users_id_fk", + "tableFrom": "federated_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_subject_idx": { + "name": "provider_subject_idx", + "nullsNotDistinct": false, + "columns": [ + "provider", + "subject" + ] + } + } + }, + "forum_questions": { + "name": "forum_questions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_title": { + "name": "question_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "question_sub_title": { + "name": "question_sub_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "forum_questions_cycle_id_cycles_id_fk": { + "name": "forum_questions_cycle_id_cycles_id_fk", + "tableFrom": "forum_questions", + "tableTo": "cycles", + "columnsFrom": [ + "cycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "group_categories": { + "name": "group_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_can_create": { + "name": "user_can_create", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_can_view": { + "name": "user_can_view", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "group_categories_event_id_events_id_fk": { + "name": "group_categories_event_id_events_id_fk", + "tableFrom": "group_categories", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "groups": { + "name": "groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "groups_group_category_id_group_categories_id_fk": { + "name": "groups_group_category_id_group_categories_id_fk", + "tableFrom": "groups", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "groups_secret_unique": { + "name": "groups_secret_unique", + "nullsNotDistinct": false, + "columns": [ + "secret" + ] + } + } + }, + "users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram": { + "name": "telegram", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_telegram_unique": { + "name": "users_telegram_unique", + "nullsNotDistinct": false, + "columns": [ + "telegram" + ] + } + } + }, + "registrations": { + "name": "registrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'DRAFT'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registrations_user_id_users_id_fk": { + "name": "registrations_user_id_users_id_fk", + "tableFrom": "registrations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registrations_event_id_events_id_fk": { + "name": "registrations_event_id_events_id_fk", + "tableFrom": "registrations", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registrations_group_id_groups_id_fk": { + "name": "registrations_group_id_groups_id_fk", + "tableFrom": "registrations", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_field_options": { + "name": "registration_field_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_field_id": { + "name": "registration_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_field_options_registration_field_id_registration_fields_id_fk": { + "name": "registration_field_options_registration_field_id_registration_fields_id_fk", + "tableFrom": "registration_field_options", + "tableTo": "registration_fields", + "columnsFrom": [ + "registration_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "question_options": { + "name": "question_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "registration_id": { + "name": "registration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_title": { + "name": "option_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "option_sub_title": { + "name": "option_sub_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "accepted": { + "name": "accepted", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vote_score": { + "name": "vote_score", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0.0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "question_options_user_id_users_id_fk": { + "name": "question_options_user_id_users_id_fk", + "tableFrom": "question_options", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "question_options_registration_id_registrations_id_fk": { + "name": "question_options_registration_id_registrations_id_fk", + "tableFrom": "question_options", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "question_options_question_id_forum_questions_id_fk": { + "name": "question_options_question_id_forum_questions_id_fk", + "tableFrom": "question_options", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "votes": { + "name": "votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_id": { + "name": "option_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "num_of_votes": { + "name": "num_of_votes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "votes_user_id_users_id_fk": { + "name": "votes_user_id_users_id_fk", + "tableFrom": "votes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_option_id_question_options_id_fk": { + "name": "votes_option_id_question_options_id_fk", + "tableFrom": "votes", + "tableTo": "question_options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_question_id_forum_questions_id_fk": { + "name": "votes_question_id_forum_questions_id_fk", + "tableFrom": "votes", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_fields": { + "name": "registration_fields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'TEXT'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_option_type": { + "name": "question_option_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "fields_display_rank": { + "name": "fields_display_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "character_limit": { + "name": "character_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "display_on_group_registration": { + "name": "display_on_group_registration", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_fields_event_id_events_id_fk": { + "name": "registration_fields_event_id_events_id_fk", + "tableFrom": "registration_fields", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registration_fields_question_id_forum_questions_id_fk": { + "name": "registration_fields_question_id_forum_questions_id_fk", + "tableFrom": "registration_fields", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_data": { + "name": "registration_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "registration_field_id": { + "name": "registration_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_data_registration_id_registrations_id_fk": { + "name": "registration_data_registration_id_registrations_id_fk", + "tableFrom": "registration_data", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registration_data_registration_field_id_registration_fields_id_fk": { + "name": "registration_data_registration_field_id_registration_fields_id_fk", + "tableFrom": "registration_data", + "tableTo": "registration_fields", + "columnsFrom": [ + "registration_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users_to_groups": { + "name": "users_to_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_groups_user_id_users_id_fk": { + "name": "users_to_groups_user_id_users_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_groups_group_id_groups_id_fk": { + "name": "users_to_groups_group_id_groups_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_groups_group_category_id_group_categories_id_fk": { + "name": "users_to_groups_group_category_id_group_categories_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_attributes": { + "name": "user_attributes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attribute_key": { + "name": "attribute_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "attribute_value": { + "name": "attribute_value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_attributes_user_id_users_id_fk": { + "name": "user_attributes_user_id_users_id_fk", + "tableFrom": "user_attributes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "likes_user_id_users_id_fk": { + "name": "likes_user_id_users_id_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "likes_comment_id_comments_id_fk": { + "name": "likes_comment_id_comments_id_fk", + "tableFrom": "likes", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "notification_types": { + "name": "notification_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "value": { + "name": "value", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notification_types_value_unique": { + "name": "notification_types_value_unique", + "nullsNotDistinct": false, + "columns": [ + "value" + ] + } + } + }, + "users_to_notifications": { + "name": "users_to_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notification_type_id": { + "name": "notification_type_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_notifications_user_id_users_id_fk": { + "name": "users_to_notifications_user_id_users_id_fk", + "tableFrom": "users_to_notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_notifications_notification_type_id_notification_types_id_fk": { + "name": "users_to_notifications_notification_type_id_notification_types_id_fk", + "tableFrom": "users_to_notifications", + "tableTo": "notification_types", + "columnsFrom": [ + "notification_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "questions_to_group_categories": { + "name": "questions_to_group_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "questions_to_group_categories_question_id_forum_questions_id_fk": { + "name": "questions_to_group_categories_question_id_forum_questions_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "questions_to_group_categories_group_category_id_group_categories_id_fk": { + "name": "questions_to_group_categories_group_category_id_group_categories_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 8fda86f5..c88cef6d 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -127,6 +127,13 @@ "when": 1713192184507, "tag": "0017_jazzy_revanche", "breakpoints": true + }, + { + "idx": 18, + "version": "5", + "when": 1713278183377, + "tag": "0018_abandoned_mongoose", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/alerts.ts b/src/db/alerts.ts new file mode 100644 index 00000000..5ed0c07d --- /dev/null +++ b/src/db/alerts.ts @@ -0,0 +1,15 @@ +import { boolean, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; + +export const alerts = pgTable('alerts', { + id: uuid('id').primaryKey().defaultRandom(), + title: varchar('title', { length: 256 }).notNull(), + description: varchar('description', { length: 1024 }), + link: varchar('link', { length: 256 }), + startAt: timestamp('start_at'), + endAt: timestamp('end_at'), + active: boolean('active').default(false), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), +}); + +export type Alert = typeof alerts.$inferSelect; diff --git a/src/db/index.ts b/src/db/index.ts index 30640a3b..c45e57b0 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -18,3 +18,4 @@ export * from './notificationTypes'; export * from './usersToNotifications'; export * from './groupCategories'; export * from './questionsToGroupCategories'; +export * from './alerts'; diff --git a/src/handlers/alerts.ts b/src/handlers/alerts.ts new file mode 100644 index 00000000..f09d226a --- /dev/null +++ b/src/handlers/alerts.ts @@ -0,0 +1,22 @@ +import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import type { Request, Response } from 'express'; +import * as db from '../db'; +import { and, eq, gte, lte, or } from 'drizzle-orm'; + +export function getActiveAlerts(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + try { + const alerts = await dbPool.query.alerts.findMany({ + where: or( + eq(db.alerts.active, true), + and(lte(db.alerts.startAt, new Date()), gte(db.alerts.endAt, new Date())), + ), + }); + + return res.json({ data: alerts }); + } catch (e) { + console.error(`[ERROR] ${JSON.stringify(e)}`); + return res.sendStatus(500); + } + }; +} diff --git a/src/routers/alerts.ts b/src/routers/alerts.ts new file mode 100644 index 00000000..3b8737a9 --- /dev/null +++ b/src/routers/alerts.ts @@ -0,0 +1,11 @@ +import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import { default as express } from 'express'; +import type * as db from '../db'; +import { getActiveAlerts } from '../handlers/alerts'; +import { isLoggedIn } from '../middleware/isLoggedIn'; +const router = express.Router(); + +export function alertsRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { + router.get('/', isLoggedIn(dbPool), getActiveAlerts(dbPool)); + return router; +} diff --git a/src/routers/api.ts b/src/routers/api.ts index 6fc844cf..b8441be8 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -15,6 +15,7 @@ import { votesRouter } from './votes'; import { registrationsRouter } from './registrations'; import { usersToGroupsRouter } from './usersToGroups'; import { groupCategoriesRouter } from './groupCategories'; +import { alertsRouter } from './alerts'; const router = express.Router(); @@ -60,6 +61,7 @@ export function apiRouter({ router.use('/group-categories', groupCategoriesRouter({ dbPool })); router.use('/registrations', registrationsRouter({ dbPool })); router.use('/users-to-groups', usersToGroupsRouter({ dbPool })); + router.use('/alerts', alertsRouter({ dbPool })); return router; } From d1db1b8a0ca5cd209901f8da779d2f1415876abd Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Tue, 16 Apr 2024 11:00:43 -0500 Subject: [PATCH 16/21] fix build command (#329) --- src/routers/groups.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/routers/groups.ts b/src/routers/groups.ts index e5059079..4e158dbb 100644 --- a/src/routers/groups.ts +++ b/src/routers/groups.ts @@ -2,15 +2,10 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { default as express } from 'express'; import type * as db from '../db'; import { isLoggedIn } from '../middleware/isLoggedIn'; -import { - createGroupHandler, - getGroupRegistrationsHandler, - getGroupsHandler, -} from '../handlers/groups'; +import { createGroupHandler, getGroupRegistrationsHandler } from '../handlers/groups'; const router = express.Router(); export function groupsRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { - router.get('/', isLoggedIn(dbPool), getGroupsHandler(dbPool)); router.post('/', isLoggedIn(dbPool), createGroupHandler(dbPool)); router.get('/:id/registrations', isLoggedIn(dbPool), getGroupRegistrationsHandler(dbPool)); From dcd6761432585b0fa8300f261ee954cd0706054a Mon Sep 17 00:00:00 2001 From: Martin Benedikt Busch <43137759+MartinBenediktBusch@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:51:42 +0200 Subject: [PATCH 17/21] seed a group with group category secrets (#330) --- src/utils/db/seed.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/db/seed.ts b/src/utils/db/seed.ts index 0746a35c..317c8615 100644 --- a/src/utils/db/seed.ts +++ b/src/utils/db/seed.ts @@ -14,6 +14,7 @@ async function seed(dbPool: PostgresJsDatabase) { groupCategories[0]?.id, groupCategories[1]?.id, groupCategories[2]?.id, + groupCategories[3]?.id, ); const users = await createUsers(dbPool); const usersToGroups = await createUsersToGroups( @@ -196,6 +197,7 @@ async function createGroups( baselineCategory?: string, categoryOne?: string, categoryTwo?: string, + secretCategory?: string, ) { return dbPool .insert(db.groups) @@ -216,6 +218,10 @@ async function createGroups( name: randCompanyName(), groupCategoryId: categoryTwo, }, + { + name: randCompanyName(), + groupCategoryId: secretCategory, + }, ]) .returning(); } From bfbf6d58b880c048be1faabbf4c2a7f82c597b94 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Tue, 16 Apr 2024 15:56:54 -0500 Subject: [PATCH 18/21] fix groupId not saving (#331) --- src/handlers/events.ts | 2 +- src/handlers/registrations.ts | 4 ++-- src/services/registrations.ts | 11 +++++------ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/handlers/events.ts b/src/handlers/events.ts index c582026c..d0575f0b 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -66,7 +66,7 @@ export function getEventRegistrationFieldsHandler(dbPool: PostgresJsDatabase eq(fields.id, eventId), + where: eq(db.events.id, eventId), }); return res.json({ data: event?.registrationFields }); diff --git a/src/handlers/registrations.ts b/src/handlers/registrations.ts index 217f5871..2532a1a6 100644 --- a/src/handlers/registrations.ts +++ b/src/handlers/registrations.ts @@ -4,6 +4,7 @@ import * as db from '../db'; import { insertRegistrationSchema } from '../types'; import { validateRequiredRegistrationFields } from '../services/registrationFields'; import { saveRegistration, updateRegistration } from '../services/registrations'; +import { and, eq } from 'drizzle-orm'; export function getRegistrationDataHandler(dbPool: PostgresJsDatabase) { return async function (req: Request, res: Response) { @@ -22,8 +23,7 @@ export function getRegistrationDataHandler(dbPool: PostgresJsDatabase with: { registrationData: true, }, - where: (fields, { eq, and }) => - and(eq(fields.userId, userId), eq(fields.id, registrationId)), + where: and(eq(db.registrations.userId, userId), eq(db.registrations.id, registrationId)), }); const out = [...(registration?.registrationData ?? [])]; diff --git a/src/services/registrations.ts b/src/services/registrations.ts index 8cb255fe..4ad707a2 100644 --- a/src/services/registrations.ts +++ b/src/services/registrations.ts @@ -1,6 +1,5 @@ -import { eq } from 'drizzle-orm'; import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import { and } from 'drizzle-orm'; +import { and, eq } from 'drizzle-orm'; import { z } from 'zod'; import { insertRegistrationSchema } from '../types'; import * as db from '../db'; @@ -16,8 +15,7 @@ export async function saveRegistration( ) { if (data.groupId) { const userGroup = dbPool.query.usersToGroups.findFirst({ - where: (fields, { eq, and }) => - and(eq(fields.userId, userId), eq(fields.groupId, data.groupId!)), + where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, data.groupId!)), }); if (!userGroup) { @@ -64,8 +62,7 @@ export async function updateRegistration({ }) { if (data.groupId) { const userGroup = dbPool.query.usersToGroups.findFirst({ - where: (fields, { eq, and }) => - and(eq(fields.userId, userId), eq(fields.groupId, data.groupId!)), + where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, data.groupId!)), }); if (!userGroup) { @@ -118,6 +115,7 @@ async function createRegistrationInDB( .insert(db.registrations) .values({ userId: body.userId, + groupId: body.groupId, eventId: body.eventId, status: body.status, }) @@ -135,6 +133,7 @@ async function updateRegistrationInDB( .set({ userId: body.userId, eventId: body.eventId, + groupId: body.groupId, status: body.status, updatedAt: new Date(), }) From afafdfdcd97e6d50b0bc2dba6ff54f51e4830b51 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Thu, 18 Apr 2024 09:27:25 -0500 Subject: [PATCH 19/21] add for_user and for_group in registration field (#338) * add migration for boolean registration field flags * add validation for user and group registration fields --- migrations/0019_nifty_wallow.sql | 2 + migrations/meta/0019_snapshot.json | 1637 +++++++++++++++++++++++ migrations/meta/_journal.json | 7 + src/db/registrationFields.ts | 3 +- src/handlers/registrations.ts | 14 +- src/services/registrationFields.spec.ts | 132 ++ src/services/registrationFields.ts | 28 +- src/utils/db/seed.ts | 4 + 8 files changed, 1817 insertions(+), 10 deletions(-) create mode 100644 migrations/0019_nifty_wallow.sql create mode 100644 migrations/meta/0019_snapshot.json create mode 100644 src/services/registrationFields.spec.ts diff --git a/migrations/0019_nifty_wallow.sql b/migrations/0019_nifty_wallow.sql new file mode 100644 index 00000000..8f5e6a4a --- /dev/null +++ b/migrations/0019_nifty_wallow.sql @@ -0,0 +1,2 @@ +ALTER TABLE "registration_fields" RENAME COLUMN "display_on_group_registration" TO "for_group";--> statement-breakpoint +ALTER TABLE "registration_fields" ADD COLUMN "for_user" boolean DEFAULT true; \ No newline at end of file diff --git a/migrations/meta/0019_snapshot.json b/migrations/meta/0019_snapshot.json new file mode 100644 index 00000000..406b19f9 --- /dev/null +++ b/migrations/meta/0019_snapshot.json @@ -0,0 +1,1637 @@ +{ + "id": "9c2a63ef-990b-4651-b9b2-b6e490f83942", + "prevId": "9e82519c-4853-44bf-ac83-165a4e51c435", + "version": "5", + "dialect": "pg", + "tables": { + "alerts": { + "name": "alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_at": { + "name": "end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_option_id": { + "name": "question_option_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comments_question_option_id_question_options_id_fk": { + "name": "comments_question_option_id_question_options_id_fk", + "tableFrom": "comments", + "tableTo": "question_options", + "columnsFrom": [ + "question_option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "cycles": { + "name": "cycles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'UPCOMING'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cycles_event_id_events_id_fk": { + "name": "cycles_event_id_events_id_fk", + "tableFrom": "cycles", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "registration_description": { + "name": "registration_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_display_rank": { + "name": "event_display_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "federated_credentials": { + "name": "federated_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federated_credentials_user_id_users_id_fk": { + "name": "federated_credentials_user_id_users_id_fk", + "tableFrom": "federated_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_subject_idx": { + "name": "provider_subject_idx", + "nullsNotDistinct": false, + "columns": [ + "provider", + "subject" + ] + } + } + }, + "forum_questions": { + "name": "forum_questions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_title": { + "name": "question_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "question_sub_title": { + "name": "question_sub_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "forum_questions_cycle_id_cycles_id_fk": { + "name": "forum_questions_cycle_id_cycles_id_fk", + "tableFrom": "forum_questions", + "tableTo": "cycles", + "columnsFrom": [ + "cycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "group_categories": { + "name": "group_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_can_create": { + "name": "user_can_create", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_can_view": { + "name": "user_can_view", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "group_categories_event_id_events_id_fk": { + "name": "group_categories_event_id_events_id_fk", + "tableFrom": "group_categories", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "groups": { + "name": "groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "groups_group_category_id_group_categories_id_fk": { + "name": "groups_group_category_id_group_categories_id_fk", + "tableFrom": "groups", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "groups_secret_unique": { + "name": "groups_secret_unique", + "nullsNotDistinct": false, + "columns": [ + "secret" + ] + } + } + }, + "users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "telegram": { + "name": "telegram", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_telegram_unique": { + "name": "users_telegram_unique", + "nullsNotDistinct": false, + "columns": [ + "telegram" + ] + } + } + }, + "registrations": { + "name": "registrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'DRAFT'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registrations_user_id_users_id_fk": { + "name": "registrations_user_id_users_id_fk", + "tableFrom": "registrations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registrations_event_id_events_id_fk": { + "name": "registrations_event_id_events_id_fk", + "tableFrom": "registrations", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registrations_group_id_groups_id_fk": { + "name": "registrations_group_id_groups_id_fk", + "tableFrom": "registrations", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_field_options": { + "name": "registration_field_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_field_id": { + "name": "registration_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_field_options_registration_field_id_registration_fields_id_fk": { + "name": "registration_field_options_registration_field_id_registration_fields_id_fk", + "tableFrom": "registration_field_options", + "tableTo": "registration_fields", + "columnsFrom": [ + "registration_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "question_options": { + "name": "question_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "registration_id": { + "name": "registration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_title": { + "name": "option_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "option_sub_title": { + "name": "option_sub_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "accepted": { + "name": "accepted", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vote_score": { + "name": "vote_score", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0.0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "question_options_user_id_users_id_fk": { + "name": "question_options_user_id_users_id_fk", + "tableFrom": "question_options", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "question_options_registration_id_registrations_id_fk": { + "name": "question_options_registration_id_registrations_id_fk", + "tableFrom": "question_options", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "question_options_question_id_forum_questions_id_fk": { + "name": "question_options_question_id_forum_questions_id_fk", + "tableFrom": "question_options", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "votes": { + "name": "votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_id": { + "name": "option_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "num_of_votes": { + "name": "num_of_votes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "votes_user_id_users_id_fk": { + "name": "votes_user_id_users_id_fk", + "tableFrom": "votes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_option_id_question_options_id_fk": { + "name": "votes_option_id_question_options_id_fk", + "tableFrom": "votes", + "tableTo": "question_options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_question_id_forum_questions_id_fk": { + "name": "votes_question_id_forum_questions_id_fk", + "tableFrom": "votes", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_fields": { + "name": "registration_fields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'TEXT'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_option_type": { + "name": "question_option_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "fields_display_rank": { + "name": "fields_display_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "character_limit": { + "name": "character_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "for_group": { + "name": "for_group", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "for_user": { + "name": "for_user", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_fields_event_id_events_id_fk": { + "name": "registration_fields_event_id_events_id_fk", + "tableFrom": "registration_fields", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registration_fields_question_id_forum_questions_id_fk": { + "name": "registration_fields_question_id_forum_questions_id_fk", + "tableFrom": "registration_fields", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "registration_data": { + "name": "registration_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "registration_field_id": { + "name": "registration_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_data_registration_id_registrations_id_fk": { + "name": "registration_data_registration_id_registrations_id_fk", + "tableFrom": "registration_data", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registration_data_registration_field_id_registration_fields_id_fk": { + "name": "registration_data_registration_field_id_registration_fields_id_fk", + "tableFrom": "registration_data", + "tableTo": "registration_fields", + "columnsFrom": [ + "registration_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users_to_groups": { + "name": "users_to_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_groups_user_id_users_id_fk": { + "name": "users_to_groups_user_id_users_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_groups_group_id_groups_id_fk": { + "name": "users_to_groups_group_id_groups_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_groups_group_category_id_group_categories_id_fk": { + "name": "users_to_groups_group_category_id_group_categories_id_fk", + "tableFrom": "users_to_groups", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_attributes": { + "name": "user_attributes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attribute_key": { + "name": "attribute_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "attribute_value": { + "name": "attribute_value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_attributes_user_id_users_id_fk": { + "name": "user_attributes_user_id_users_id_fk", + "tableFrom": "user_attributes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "likes_user_id_users_id_fk": { + "name": "likes_user_id_users_id_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "likes_comment_id_comments_id_fk": { + "name": "likes_comment_id_comments_id_fk", + "tableFrom": "likes", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "notification_types": { + "name": "notification_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "value": { + "name": "value", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notification_types_value_unique": { + "name": "notification_types_value_unique", + "nullsNotDistinct": false, + "columns": [ + "value" + ] + } + } + }, + "users_to_notifications": { + "name": "users_to_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notification_type_id": { + "name": "notification_type_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_notifications_user_id_users_id_fk": { + "name": "users_to_notifications_user_id_users_id_fk", + "tableFrom": "users_to_notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_notifications_notification_type_id_notification_types_id_fk": { + "name": "users_to_notifications_notification_type_id_notification_types_id_fk", + "tableFrom": "users_to_notifications", + "tableTo": "notification_types", + "columnsFrom": [ + "notification_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "questions_to_group_categories": { + "name": "questions_to_group_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_category_id": { + "name": "group_category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "questions_to_group_categories_question_id_forum_questions_id_fk": { + "name": "questions_to_group_categories_question_id_forum_questions_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "forum_questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "questions_to_group_categories_group_category_id_group_categories_id_fk": { + "name": "questions_to_group_categories_group_category_id_group_categories_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "group_categories", + "columnsFrom": [ + "group_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index c88cef6d..29a0acb2 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -134,6 +134,13 @@ "when": 1713278183377, "tag": "0018_abandoned_mongoose", "breakpoints": true + }, + { + "idx": 19, + "version": "5", + "when": 1713387423686, + "tag": "0019_nifty_wallow", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/registrationFields.ts b/src/db/registrationFields.ts index 47855f5b..1446fb45 100644 --- a/src/db/registrationFields.ts +++ b/src/db/registrationFields.ts @@ -17,7 +17,8 @@ export const registrationFields = pgTable('registration_fields', { questionOptionType: varchar('question_option_type'), fieldDisplayRank: integer('fields_display_rank'), characterLimit: integer('character_limit').default(0), - displayOnGroupRegistration: boolean('display_on_group_registration').default(false), + forGroup: boolean('for_group').default(false), + forUser: boolean('for_user').default(true), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); diff --git a/src/handlers/registrations.ts b/src/handlers/registrations.ts index 2532a1a6..05e883ba 100644 --- a/src/handlers/registrations.ts +++ b/src/handlers/registrations.ts @@ -45,7 +45,12 @@ export function saveRegistrationHandler(dbPool: PostgresJsDatabase) { return res.status(400).json({ errors: body.error.issues }); } - const missingRequiredFields = await validateRequiredRegistrationFields(dbPool, body.data); + const missingRequiredFields = await validateRequiredRegistrationFields({ + dbPool, + data: body.data, + forGroup: !!body.data.groupId, + forUser: !body.data.groupId, + }); if (missingRequiredFields.length > 0) { return res.status(400).json({ errors: missingRequiredFields }); @@ -77,7 +82,12 @@ export function updateRegistrationHandler(dbPool: PostgresJsDatabase) return res.status(400).json({ errors: body.error.issues }); } - const missingRequiredFields = await validateRequiredRegistrationFields(dbPool, body.data); + const missingRequiredFields = await validateRequiredRegistrationFields({ + dbPool, + data: body.data, + forGroup: !!body.data.groupId, + forUser: !body.data.groupId, + }); if (missingRequiredFields.length > 0) { return res.status(400).json({ errors: missingRequiredFields }); diff --git a/src/services/registrationFields.spec.ts b/src/services/registrationFields.spec.ts new file mode 100644 index 00000000..1f0bf772 --- /dev/null +++ b/src/services/registrationFields.spec.ts @@ -0,0 +1,132 @@ +import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import { z } from 'zod'; +import * as db from '../db'; +import { insertRegistrationSchema } from '../types'; +import { createDbPool } from '../utils/db/createDbPool'; +import { runMigrations } from '../utils/db/runMigrations'; +import { cleanup, seed } from '../utils/db/seed'; +import { validateRequiredRegistrationFields } from './registrationFields'; + +const DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432'; + +describe('service: registrationFields', () => { + let dbPool: PostgresJsDatabase; + let dbConnection: postgres.Sql>; + let requiredByGroupRegistrationField: db.RegistrationField | undefined; + let requiredByUserRegistrationField: db.RegistrationField | undefined; + let testRegistration: z.infer; + + beforeAll(async () => { + const initDb = createDbPool(DB_CONNECTION_URL, { max: 1 }); + await runMigrations(DB_CONNECTION_URL); + dbPool = initDb.dbPool; + dbConnection = initDb.connection; + // seed + const { events, users, registrationFields } = await seed(dbPool); + + // required by group + requiredByGroupRegistrationField = registrationFields[0]; + // required by user + requiredByUserRegistrationField = registrationFields[1]; + + testRegistration = { + userId: users[0]?.id ?? '', + eventId: events[0]?.id ?? '', + status: 'DRAFT', + registrationData: [ + { + registrationFieldId: registrationFields[0]?.id ?? '', + value: 'title', + }, + { + registrationFieldId: registrationFields[1]?.id ?? '', + value: 'sub title', + }, + { + registrationFieldId: registrationFields[2]?.id ?? '', + value: 'other', + }, + ], + }; + }); + + describe('should return an empty array if all required fields are filled', function () { + test('for user', async () => { + const missingRequiredFields = await validateRequiredRegistrationFields({ + dbPool, + data: { + ...testRegistration, + registrationData: testRegistration.registrationData.filter( + (data) => data.registrationFieldId !== requiredByGroupRegistrationField?.id, + ), + }, + forGroup: false, + forUser: true, + }); + expect(missingRequiredFields).toEqual([]); + }); + test('for group', async () => { + const missingRequiredFields = await validateRequiredRegistrationFields({ + dbPool, + data: { + ...testRegistration, + registrationData: testRegistration.registrationData.filter( + (data) => data.registrationFieldId !== requiredByUserRegistrationField?.id, + ), + }, + forGroup: true, + forUser: false, + }); + expect(missingRequiredFields).toEqual([]); + }); + }); + + describe('should return an array of missing required fields', function () { + test('for user', async () => { + const missingRequiredFields = await validateRequiredRegistrationFields({ + dbPool, + data: { + ...testRegistration, + registrationData: testRegistration.registrationData.filter( + (data) => data.registrationFieldId !== requiredByUserRegistrationField?.id, + ), + }, + forGroup: false, + forUser: true, + }); + + expect(missingRequiredFields).toEqual([ + { + field: requiredByUserRegistrationField?.name, + message: 'missing required field', + }, + ]); + }); + test('for group', async () => { + const missingRequiredFields = await validateRequiredRegistrationFields({ + dbPool, + data: { + ...testRegistration, + registrationData: testRegistration.registrationData.filter( + (data) => data.registrationFieldId !== requiredByGroupRegistrationField?.id, + ), + }, + forGroup: true, + forUser: false, + }); + + expect(missingRequiredFields).toEqual([ + { + field: requiredByGroupRegistrationField?.name, + message: 'missing required field', + }, + ]); + }); + }); + + afterAll(async () => { + await cleanup(dbPool); + await dbConnection.end(); + }); +}); diff --git a/src/services/registrationFields.ts b/src/services/registrationFields.ts index c65c3283..d94bd6b9 100644 --- a/src/services/registrationFields.ts +++ b/src/services/registrationFields.ts @@ -1,24 +1,38 @@ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import * as db from '../db'; +import { and, eq } from 'drizzle-orm'; -export async function validateRequiredRegistrationFields( - dbPool: PostgresJsDatabase, +export async function validateRequiredRegistrationFields({ + data, + dbPool, + forGroup, + forUser, +}: { + dbPool: PostgresJsDatabase; data: { eventId: string; registrationData: { registrationFieldId: string; value: string; }[]; - }, -) { + }; + forUser: boolean; + forGroup: boolean; +}) { // check if all required fields are filled const event = await dbPool.query.events.findFirst({ with: { - registrationFields: true, + registrationFields: { + where: and( + eq(db.registrationFields.forUser, forUser), + eq(db.registrationFields.forGroup, forGroup), + eq(db.registrationFields.required, true), + ), + }, }, - where: (event, { eq }) => eq(event.id, data.eventId), + where: eq(db.events.id, data.eventId), }); - const requiredFields = event?.registrationFields.filter((field) => field.required); + const requiredFields = event?.registrationFields; if (!requiredFields) { return []; diff --git a/src/utils/db/seed.ts b/src/utils/db/seed.ts index 317c8615..a8ed2b24 100644 --- a/src/utils/db/seed.ts +++ b/src/utils/db/seed.ts @@ -86,6 +86,8 @@ async function createRegistrationFields(dbPool: PostgresJsDatabase, e required: true, eventId, questionOptionType: 'TITLE', + forUser: false, + forGroup: true, }, { name: 'proposal description', @@ -93,6 +95,8 @@ async function createRegistrationFields(dbPool: PostgresJsDatabase, e required: true, eventId, questionOptionType: 'SUBTITLE', + forUser: true, + forGroup: false, }, { name: 'other field', From 77b8d4149380193b691e0e5a296cb13d55b90ba9 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Thu, 18 Apr 2024 09:28:51 -0500 Subject: [PATCH 20/21] Limit one registration per group (#339) * limit one registration per group * clean up logic * add authorization on update registration * fix authorization on an event level --- src/handlers/registrations.ts | 29 +++++++++++- src/services/registrations.ts | 86 +++++++++++++++++++++++++++-------- 2 files changed, 95 insertions(+), 20 deletions(-) diff --git a/src/handlers/registrations.ts b/src/handlers/registrations.ts index 05e883ba..c0904170 100644 --- a/src/handlers/registrations.ts +++ b/src/handlers/registrations.ts @@ -3,7 +3,12 @@ import type { Request, Response } from 'express'; import * as db from '../db'; import { insertRegistrationSchema } from '../types'; import { validateRequiredRegistrationFields } from '../services/registrationFields'; -import { saveRegistration, updateRegistration } from '../services/registrations'; +import { + saveRegistration, + updateRegistration, + validateCreateRegistrationPermissions, + validateUpdateRegistrationPermissions, +} from '../services/registrations'; import { and, eq } from 'drizzle-orm'; export function getRegistrationDataHandler(dbPool: PostgresJsDatabase) { @@ -56,6 +61,17 @@ export function saveRegistrationHandler(dbPool: PostgresJsDatabase) { return res.status(400).json({ errors: missingRequiredFields }); } + const canRegisterGroup = await validateCreateRegistrationPermissions({ + dbPool, + userId, + eventId: body.data.eventId, + groupId: body.data.groupId, + }); + + if (!canRegisterGroup) { + return res.status(400).json({ errors: ['Cannot register for this group'] }); + } + try { const out = await saveRegistration(dbPool, body.data, userId); return res.json({ data: out }); @@ -93,6 +109,17 @@ export function updateRegistrationHandler(dbPool: PostgresJsDatabase) return res.status(400).json({ errors: missingRequiredFields }); } + const canUpdateRegistration = await validateUpdateRegistrationPermissions({ + dbPool, + registrationId, + userId, + groupId: body.data.groupId, + }); + + if (!canUpdateRegistration) { + return res.status(400).json({ errors: ['Cannot update this registration'] }); + } + try { const out = await updateRegistration({ data: body.data, diff --git a/src/services/registrations.ts b/src/services/registrations.ts index 4ad707a2..f58a3c39 100644 --- a/src/services/registrations.ts +++ b/src/services/registrations.ts @@ -8,21 +8,80 @@ import { upsertQuestionOptionFromRegistrationData, } from './registrationData'; -export async function saveRegistration( - dbPool: PostgresJsDatabase, - data: z.infer, - userId: string, -) { - if (data.groupId) { +export async function validateCreateRegistrationPermissions({ + dbPool, + userId, + groupId, + eventId, +}: { + dbPool: PostgresJsDatabase; + userId: string; + eventId: string; + groupId?: string | null; +}) { + if (groupId) { + const userGroup = dbPool.query.usersToGroups.findFirst({ + where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, groupId)), + }); + + if (!userGroup) { + return false; + } + + // limit one registration per group per event + const existingRegistration = await dbPool.query.registrations.findFirst({ + where: and(eq(db.registrations.eventId, eventId), eq(db.registrations.groupId, groupId)), + }); + + if (existingRegistration) { + return false; + } + } + + return true; +} + +export async function validateUpdateRegistrationPermissions({ + dbPool, + registrationId, + userId, + groupId, +}: { + dbPool: PostgresJsDatabase; + userId: string; + registrationId: string; + groupId?: string | null; +}) { + const existingRegistration = await dbPool.query.registrations.findFirst({ + where: and(eq(db.registrations.userId, userId), eq(db.registrations.id, registrationId)), + }); + + if (!existingRegistration) { + return false; + } + + if (existingRegistration.userId !== userId) { + return false; + } + + if (groupId) { const userGroup = dbPool.query.usersToGroups.findFirst({ - where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, data.groupId!)), + where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, groupId)), }); if (!userGroup) { - throw new Error('user is not in group'); + return false; } } + return true; +} + +export async function saveRegistration( + dbPool: PostgresJsDatabase, + data: z.infer, + userId: string, +) { const newRegistration = await createRegistrationInDB(dbPool, data); if (!newRegistration) { throw new Error('failed to save registration'); @@ -60,16 +119,6 @@ export async function updateRegistration({ registrationId: string; userId: string; }) { - if (data.groupId) { - const userGroup = dbPool.query.usersToGroups.findFirst({ - where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, data.groupId!)), - }); - - if (!userGroup) { - throw new Error('user is not in group'); - } - } - const existingRegistration = await dbPool.query.registrations.findFirst({ where: and(eq(db.registrations.userId, userId), eq(db.registrations.id, registrationId)), }); @@ -131,7 +180,6 @@ async function updateRegistrationInDB( const updatedRegistration = await dbPool .update(db.registrations) .set({ - userId: body.userId, eventId: body.eventId, groupId: body.groupId, status: body.status, From 6d21fb1f9b09e5edf73dc92792f734cbe4de3093 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Fri, 19 Apr 2024 10:45:01 -0500 Subject: [PATCH 21/21] adds better validation for fields and register field options in seed (#342) * adds better validation for fields and register field options in seed * make select field not required --- src/services/registrationFields.ts | 20 ++++++++++++++--- src/utils/db/seed.ts | 36 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/services/registrationFields.ts b/src/services/registrationFields.ts index d94bd6b9..1bccc1a4 100644 --- a/src/services/registrationFields.ts +++ b/src/services/registrationFields.ts @@ -39,9 +39,23 @@ export async function validateRequiredRegistrationFields({ } // loop through required fields and check if they are filled - const missingFields = requiredFields.filter( - (field) => !data.registrationData.some((data) => data.registrationFieldId === field.id), - ); + const missingFields = requiredFields.filter((field) => { + const registrationField = data.registrationData.find( + (data) => data.registrationFieldId === field.id, + ); + + // if field is not found in registration data, it is missing + if (!registrationField) { + return true; + } + + // if field is found but value is empty, it is missing + if (!registrationField.value) { + return true; + } + + return false; + }); return missingFields.map((field) => ({ field: field.name, diff --git a/src/utils/db/seed.ts b/src/utils/db/seed.ts index a8ed2b24..70e3c218 100644 --- a/src/utils/db/seed.ts +++ b/src/utils/db/seed.ts @@ -6,6 +6,10 @@ async function seed(dbPool: PostgresJsDatabase) { const events = await createEvent(dbPool); const cycles = await createCycle(dbPool, events[0]?.id); const registrationFields = await createRegistrationFields(dbPool, events[0]?.id); + const registrationFieldOptions = await createRegistrationFieldOptions( + dbPool, + registrationFields[3]?.id, + ); const forumQuestions = await createForumQuestions(dbPool, cycles[0]?.id); const questionOptions = await createQuestionOptions(dbPool, forumQuestions[0]?.id); const groupCategories = await createGroupCategories(dbPool, events[0]?.id); @@ -42,6 +46,7 @@ async function seed(dbPool: PostgresJsDatabase) { usersToGroups, registrationFields, questionsToGroupCategories, + registrationFieldOptions, }; } @@ -51,6 +56,7 @@ async function cleanup(dbPool: PostgresJsDatabase) { await dbPool.delete(db.federatedCredentials); await dbPool.delete(db.questionOptions); await dbPool.delete(db.registrationData); + await dbPool.delete(db.registrationFieldOptions); await dbPool.delete(db.registrationFields); await dbPool.delete(db.registrations); await dbPool.delete(db.usersToGroups); @@ -104,6 +110,36 @@ async function createRegistrationFields(dbPool: PostgresJsDatabase, e required: false, eventId, }, + { + name: 'select field', + type: 'SELECT', + required: false, + eventId, + forUser: true, + }, + ]) + .returning(); +} + +async function createRegistrationFieldOptions( + dbPool: PostgresJsDatabase, + registrationFieldId?: string, +) { + if (registrationFieldId === undefined) { + throw new Error('Registration Field ID is undefined.'); + } + + return dbPool + .insert(db.registrationFieldOptions) + .values([ + { + registrationFieldId, + value: 'Option A', + }, + { + registrationFieldId, + value: 'Option B', + }, ]) .returning(); }