diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index 9770a995..a7d05777 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -4,7 +4,7 @@ on: pull_request: types: [opened, synchronize, reopened] branches: - - next + - next-v* - develop - main @@ -32,5 +32,5 @@ jobs: - name: Install dependencies run: pnpm install - - name: Review linting - run: pnpm format + - name: Review formatting + run: pnpm format \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0ea28f69..579c3eee 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,7 +4,7 @@ on: pull_request: types: [opened, synchronize, reopened] branches: - - next + - next-v* - develop - main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6950491..40c47eb8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: pull_request: types: [opened, synchronize, reopened] branches: - - next + - next-v* - develop - main diff --git a/jest.config.js b/jest.config.js index e86b222f..d14d7fc1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,4 +8,10 @@ module.exports = { testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], testPathIgnorePatterns: ['node_modules'], preset: 'ts-jest', + coverageReporters: ['json', 'lcov', 'text', 'clover'], + collectCoverage: false, + collectCoverageFrom: [ + 'src/modules/**/*.ts', + 'src/services/**/*.ts', + ], }; diff --git a/migrations/0011_handy_night_thrasher.sql b/migrations/0011_handy_night_thrasher.sql new file mode 100644 index 00000000..1c78f63c --- /dev/null +++ b/migrations/0011_handy_night_thrasher.sql @@ -0,0 +1,29 @@ +CREATE TABLE IF NOT EXISTS "notification_types" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "value" varchar(256), + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "notification_types_value_unique" UNIQUE("value") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "users_to_notifications" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid, + "notification_type_id" uuid, + "active" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN IF EXISTS "email_notification";--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "users_to_notifications" ADD CONSTRAINT "users_to_notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "users_to_notifications" ADD CONSTRAINT "users_to_notifications_notification_type_id_notification_types_id_fk" FOREIGN KEY ("notification_type_id") REFERENCES "notification_types"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/migrations/meta/0011_snapshot.json b/migrations/meta/0011_snapshot.json new file mode 100644 index 00000000..32a502c2 --- /dev/null +++ b/migrations/meta/0011_snapshot.json @@ -0,0 +1,1317 @@ +{ + "id": "a41f993d-7bfb-48d2-ab8a-2dc194219e6b", + "prevId": "4a176102-b309-4a23-93af-121af80be7ef", + "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 + }, + "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": {} + }, + "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 + }, + "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": { + "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 + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "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" + ] + } + } + }, + "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 + }, + "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" + } + }, + "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": {} + } + }, + "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 da2a507d..8d799968 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1709135839413, "tag": "0010_great_lord_tyger", "breakpoints": true + }, + { + "idx": 11, + "version": "5", + "when": 1709219168371, + "tag": "0011_handy_night_thrasher", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 036a4e99..fcfe8ee3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "forum", - "version": "2.1.6", + "version": "2.2.0", "description": "", "main": "dist/index.js", "scripts": { @@ -20,6 +20,7 @@ "db:seed:cleanup": "pnpm run db:seed --cleanup", "db:insert:groups": "pnpx esbuild ./scripts/db/insertCustomGroups.ts --bundle --platform=node --format=cjs | node --env-file=.env", "test": "jest --runInBand", + "test:coverage": "jest --coverage --runInBand", "format": "prettier --check \"src/**/*.{ts,md}\"", "format:fix": "prettier --write \"src/**/*.{ts,md}\"" }, @@ -63,4 +64,4 @@ "postgres": "^3.4.3", "zod": "^3.22.4" } -} +} \ No newline at end of file diff --git a/src/db/index.ts b/src/db/index.ts index 79cd3357..251c6637 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -14,3 +14,5 @@ export * from './usersToGroups'; export * from './userAttributes'; export * from './comments'; export * from './likes'; +export * from './notificationTypes'; +export * from './usersToNotifications'; diff --git a/src/db/notificationTypes.ts b/src/db/notificationTypes.ts new file mode 100644 index 00000000..c3e31ccb --- /dev/null +++ b/src/db/notificationTypes.ts @@ -0,0 +1,10 @@ +import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; + +export const notificationTypes = pgTable('notification_types', { + id: uuid('id').primaryKey().defaultRandom(), + value: varchar('value', { length: 256 }).unique(), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), +}); + +export type NotificationType = typeof notificationTypes.$inferSelect; diff --git a/src/db/users.ts b/src/db/users.ts index db0992da..d9e75f70 100644 --- a/src/db/users.ts +++ b/src/db/users.ts @@ -1,20 +1,20 @@ import { relations } from 'drizzle-orm'; -import { boolean, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; -import { registrations } from './registrations'; -import { votes } from './votes'; -import { usersToGroups } from './usersToGroups'; -import { userAttributes } from './userAttributes'; +import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import { federatedCredentials } from '.'; import { comments } from './comments'; import { likes } from './likes'; import { questionOptions } from './questionOptions'; +import { registrations } from './registrations'; +import { userAttributes } from './userAttributes'; +import { usersToGroups } from './usersToGroups'; +import { votes } from './votes'; +import { usersToNotifications } from './usersToNotifications'; export const users = pgTable('users', { id: uuid('id').primaryKey().defaultRandom(), username: varchar('username', { length: 256 }).unique(), name: varchar('name'), email: varchar('email', { length: 256 }).unique(), - emailNotification: boolean('email_notification').default(true), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); @@ -28,6 +28,7 @@ export const usersRelations = relations(users, ({ many, one }) => ({ comments: many(comments), likes: many(likes), questionOptions: many(questionOptions), + notifications: many(usersToNotifications), })); export type User = typeof users.$inferSelect; diff --git a/src/db/usersToNotifications.ts b/src/db/usersToNotifications.ts new file mode 100644 index 00000000..63d56bf1 --- /dev/null +++ b/src/db/usersToNotifications.ts @@ -0,0 +1,26 @@ +import { boolean, pgTable, timestamp, uuid } from 'drizzle-orm/pg-core'; +import { users } from './users'; +import { notificationTypes } from './notificationTypes'; +import { relations } from 'drizzle-orm'; + +export const usersToNotifications = pgTable('users_to_notifications', { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').references(() => users.id), + notificationTypeId: uuid('notification_type_id').references(() => notificationTypes.id), + active: boolean('active').notNull().default(true), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), +}); + +export type UsersToNotification = typeof usersToNotifications.$inferSelect; + +export const usersToNotificationsRelations = relations(usersToNotifications, ({ one }) => ({ + user: one(users, { + fields: [usersToNotifications.userId], + references: [users.id], + }), + notificationType: one(notificationTypes, { + fields: [usersToNotifications.notificationTypeId], + references: [notificationTypes.id], + }), +})); diff --git a/src/middleware/isLoggedIn.ts b/src/middleware/isLoggedIn.ts index 7fe6c874..202f2d9d 100644 --- a/src/middleware/isLoggedIn.ts +++ b/src/middleware/isLoggedIn.ts @@ -1,8 +1,22 @@ import type { NextFunction, Response, Request } from 'express'; +import * as db from '../db'; +import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import { eq } from 'drizzle-orm'; -export function isLoggedIn() { - return function (req: Request, res: Response, next: NextFunction) { +export function isLoggedIn(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response, next: NextFunction) { if (req.session?.userId) { + const rows = await dbPool + .selectDistinct({ + id: db.users.id, + }) + .from(db.users) + .where(eq(db.users.id, req.session.userId)); + + if (!rows.length) { + return res.status(401).send(); + } + next(); } else { return res.status(401).send(); diff --git a/src/routers/comments.ts b/src/routers/comments.ts index 2b875379..54b858c0 100644 --- a/src/routers/comments.ts +++ b/src/routers/comments.ts @@ -6,8 +6,8 @@ import { deleteLike, getLikes, saveLike } from '../services/likes'; const router = express.Router(); export function commentsRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { - router.get('/:commentId/likes', isLoggedIn(), getLikes(dbPool)); - router.post('/:commentId/likes', isLoggedIn(), saveLike(dbPool)); - router.delete('/:commentId/likes', isLoggedIn(), deleteLike(dbPool)); + router.get('/:commentId/likes', isLoggedIn(dbPool), getLikes(dbPool)); + router.post('/:commentId/likes', isLoggedIn(dbPool), saveLike(dbPool)); + router.delete('/:commentId/likes', isLoggedIn(dbPool), deleteLike(dbPool)); return router; } diff --git a/src/routers/cycles.ts b/src/routers/cycles.ts index 8d67792f..124d9cd5 100644 --- a/src/routers/cycles.ts +++ b/src/routers/cycles.ts @@ -8,9 +8,9 @@ import { getVotes, saveVotes } from '../services/votes'; const router = express.Router(); export function cyclesRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { - router.get('/', isLoggedIn(), getActiveCycles(dbPool)); - router.get('/:cycleId', isLoggedIn(), getCycleById(dbPool)); - router.get('/:cycleId/votes', isLoggedIn(), getVotes(dbPool)); - router.post('/:cycleId/votes', isLoggedIn(), saveVotes(dbPool)); + router.get('/', isLoggedIn(dbPool), getActiveCycles(dbPool)); + router.get('/:cycleId', isLoggedIn(dbPool), getCycleById(dbPool)); + router.get('/:cycleId/votes', isLoggedIn(dbPool), getVotes(dbPool)); + router.post('/:cycleId/votes', isLoggedIn(dbPool), saveVotes(dbPool)); return router; } diff --git a/src/routers/events.ts b/src/routers/events.ts index 5bf39f1a..3b2d55be 100644 --- a/src/routers/events.ts +++ b/src/routers/events.ts @@ -10,12 +10,12 @@ import { getEventCycles } from '../services/cycles'; const router = express.Router(); export function eventsRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { - router.get('/', isLoggedIn(), getEvents(dbPool)); - router.get('/:eventId', isLoggedIn(), getEvent(dbPool)); - router.get('/:eventId/registration-fields', isLoggedIn(), getRegistrationFields(dbPool)); - router.get('/:eventId/registration-data', isLoggedIn(), getRegistrationData(dbPool)); - router.post('/:eventId/registration', isLoggedIn(), saveRegistration(dbPool)); - router.get('/:eventId/registration', isLoggedIn(), getRegistration(dbPool)); - router.get('/:eventId/cycles', isLoggedIn(), getEventCycles(dbPool)); + router.get('/', isLoggedIn(dbPool), getEvents(dbPool)); + router.get('/:eventId', isLoggedIn(dbPool), getEvent(dbPool)); + router.get('/:eventId/registration-fields', isLoggedIn(dbPool), getRegistrationFields(dbPool)); + router.get('/:eventId/registration-data', isLoggedIn(dbPool), getRegistrationData(dbPool)); + router.post('/:eventId/registration', isLoggedIn(dbPool), saveRegistration(dbPool)); + router.get('/:eventId/registration', isLoggedIn(dbPool), getRegistration(dbPool)); + router.get('/:eventId/cycles', isLoggedIn(dbPool), getEventCycles(dbPool)); return router; } diff --git a/src/routers/forumQuestions.ts b/src/routers/forumQuestions.ts index 679ba522..3334c65b 100644 --- a/src/routers/forumQuestions.ts +++ b/src/routers/forumQuestions.ts @@ -2,12 +2,12 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { default as express } from 'express'; import type * as db from '../db'; import { getQuestionHearts } from '../services/forumQuestions'; -import { getResultStatistics } from '../services/resultsPage'; +import { getResultStatistics } from '../services/statistics'; import { isLoggedIn } from '../middleware/isLoggedIn'; const router = express.Router(); export function forumQuestionsRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { - router.get('/:forumQuestionId/hearts', isLoggedIn(), getQuestionHearts(dbPool)); - router.get('/:forumQuestionId/statistics', isLoggedIn(), getResultStatistics(dbPool)); + router.get('/:forumQuestionId/hearts', isLoggedIn(dbPool), getQuestionHearts(dbPool)); + router.get('/:forumQuestionId/statistics', isLoggedIn(dbPool), getResultStatistics(dbPool)); return router; } diff --git a/src/routers/groups.ts b/src/routers/groups.ts index ef85c9b6..63595ffe 100644 --- a/src/routers/groups.ts +++ b/src/routers/groups.ts @@ -6,6 +6,6 @@ import { isLoggedIn } from '../middleware/isLoggedIn'; const router = express.Router(); export function groupsRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { - router.get('/', isLoggedIn(), getGroups(dbPool)); + router.get('/', isLoggedIn(dbPool), getGroups(dbPool)); return router; } diff --git a/src/routers/options.ts b/src/routers/options.ts index 4e2c553f..78828dd2 100644 --- a/src/routers/options.ts +++ b/src/routers/options.ts @@ -3,13 +3,15 @@ import { default as express } from 'express'; import type * as db from '../db'; import { isLoggedIn } from '../middleware/isLoggedIn'; import { getOption } from '../services/options'; -import { saveComment, getCommentsForOption } from '../services/comments'; +import { saveComment, getCommentsForOption, deleteComment } from '../services/comments'; const router = express.Router(); export function optionsRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { - router.get('/:optionId', isLoggedIn(), getOption(dbPool)); - router.get('/:optionId/comments', isLoggedIn(), getCommentsForOption(dbPool)); - router.post('/:optionId/comments', isLoggedIn(), saveComment(dbPool)); + router.get('/:optionId', isLoggedIn(dbPool), getOption(dbPool)); + router.get('/:optionId/comments', isLoggedIn(dbPool), getCommentsForOption(dbPool)); + router.post('/:optionId/comments', isLoggedIn(dbPool), saveComment(dbPool)); + router.delete('/:optionId/comments/:commentId', isLoggedIn(dbPool), deleteComment(dbPool)); + return router; } diff --git a/src/routers/users.ts b/src/routers/users.ts index 47423339..2aba4f7d 100644 --- a/src/routers/users.ts +++ b/src/routers/users.ts @@ -5,13 +5,15 @@ import { getUser, getUserAttributes, updateUser } from '../services/users'; import { getGroupsPerUser } from '../services/groups'; import { isLoggedIn } from '../middleware/isLoggedIn'; import { getUserOptions } from '../services/options'; +import { getUserRegistrations } from '../services/registrations'; const router = express.Router(); export function usersRouter({ dbPool }: { dbPool: PostgresJsDatabase }) { - router.get('/', isLoggedIn(), getUser(dbPool)); - router.put('/:userId', isLoggedIn(), updateUser(dbPool)); - router.get('/:userId/groups', isLoggedIn(), getGroupsPerUser(dbPool)); - router.get('/:userId/attributes', isLoggedIn(), getUserAttributes(dbPool)); - router.get('/:userId/options', isLoggedIn(), getUserOptions(dbPool)); + router.get('/', isLoggedIn(dbPool), getUser(dbPool)); + router.put('/:userId', isLoggedIn(dbPool), updateUser(dbPool)); + router.get('/:userId/groups', isLoggedIn(dbPool), getGroupsPerUser(dbPool)); + router.get('/:userId/attributes', isLoggedIn(dbPool), getUserAttributes(dbPool)); + router.get('/:userId/options', isLoggedIn(dbPool), getUserOptions(dbPool)); + router.get('/:userId/registrations', isLoggedIn(dbPool), getUserRegistrations(dbPool)); return router; } diff --git a/src/services/comments.ts b/src/services/comments.ts index 913b5001..b59e1eb5 100644 --- a/src/services/comments.ts +++ b/src/services/comments.ts @@ -1,4 +1,4 @@ -import { eq } from 'drizzle-orm'; +import { eq, and } from 'drizzle-orm'; import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import type { Request, Response } from 'express'; import { insertCommentSchema } from '../types'; @@ -58,6 +58,48 @@ export async function insertComment( } } +/** + * Deletes a comment from the database, along with associated likes if any. + * @param {PostgresJsDatabase} dbPool - The database pool connection. + * @returns {Promise} - A promise that resolves once the comment and associated likes are deleted. + * @throws {Error} - Throws an error if the deletion fails. + */ +export function deleteComment(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + const commentId = req.params.commentId; + const userId = req.session.userId; + + if (!commentId) { + return res.status(400).json({ errors: ['commentId is required'] }); + } + + try { + // Only the author of the comment has the authorization to delete the comment + const comment = await dbPool.query.comments.findFirst({ + where: and(eq(db.comments.id, commentId), eq(db.comments.userId, userId)), + }); + + if (!comment) { + return res.status(404).json({ errors: ['Unauthorized to delete comment'] }); + } + + // Delete all likes associated with the deleted comment + await dbPool.delete(db.likes).where(eq(db.likes.commentId, commentId)); + + // Delete the comment + const deletedComment = await dbPool + .delete(db.comments) + .where(eq(db.comments.id, commentId)) + .returning(); + + return res.json({ data: deletedComment }); + } catch (error) { + console.error(error); + return res.status(500).json({ errors: ['Failed to delete comment'] }); + } + }; +} + /** * Retrieves comments related to a specific question option from the database and associates them with corresponding user information. * @param {PostgresJsDatabase} dbPool - The database pool connection. diff --git a/src/services/cycles.spec.ts b/src/services/cycles.spec.ts new file mode 100644 index 00000000..eadc3cfa --- /dev/null +++ b/src/services/cycles.spec.ts @@ -0,0 +1,45 @@ +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 { GetCycleByIdFromDB } from './cycles'; +const DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432'; + +describe('service: cycles', () => { + let dbPool: PostgresJsDatabase; + let dbConnection: postgres.Sql>; + let cycle: db.Cycle | undefined; + + beforeAll(async () => { + const initDb = createDbPool(DB_CONNECTION_URL, { max: 1 }); + await runMigrations(DB_CONNECTION_URL); + dbPool = initDb.dbPool; + dbConnection = initDb.connection; + // Seed the database + const { cycles } = await seed(dbPool); + cycle = cycles[0]; + }); + + it('should get cycle by id', async () => { + const response = await GetCycleByIdFromDB(dbPool, cycle?.id ?? ''); + expect(response).toBeDefined(); + expect(response).toHaveProperty('id'); + expect(response.id).toEqual(cycle?.id); + expect(response).toHaveProperty('status'); + expect(response.status).toEqual(cycle?.status); + expect(response).toHaveProperty('forumQuestions'); + expect(response.forumQuestions).toEqual(expect.any(Array)); + expect(response.forumQuestions?.[0]?.questionOptions).toEqual(expect.any(Array)); + expect(response).toHaveProperty('createdAt'); + expect(response.createdAt).toEqual(cycle?.createdAt); + expect(response).toHaveProperty('updatedAt'); + expect(response.updatedAt).toEqual(cycle?.updatedAt); + }); + + afterAll(async () => { + await cleanup(dbPool); + await dbConnection.end(); + }); +}); diff --git a/src/services/cycles.ts b/src/services/cycles.ts index 93c6b71c..0e1edceb 100644 --- a/src/services/cycles.ts +++ b/src/services/cycles.ts @@ -55,19 +55,62 @@ export function getCycleById(dbPool: PostgresJsDatabase) { return res.status(400).json({ error: 'Missing cycleId' }); } - const cycle = await dbPool.query.cycles.findFirst({ - where: eq(db.cycles.id, cycleId), - with: { - forumQuestions: { - with: { - questionOptions: { - where: eq(db.questionOptions.accepted, true), + const out = await GetCycleByIdFromDB(dbPool, cycleId); + + return res.json({ data: out }); + }; +} + +export async function GetCycleByIdFromDB(dbPool: PostgresJsDatabase, cycleId: string) { + const cycle = await dbPool.query.cycles.findFirst({ + where: eq(db.cycles.id, cycleId), + with: { + forumQuestions: { + with: { + questionOptions: { + with: { + user: { + with: { + usersToGroups: { + with: { + group: true, + }, + }, + }, + }, }, + where: eq(db.questionOptions.accepted, true), }, }, }, - }); + }, + }); - return res.json({ data: cycle }); + const out = { + ...cycle, + forumQuestions: cycle?.forumQuestions.map((question) => { + return { + ...question, + questionOptions: question.questionOptions.map((option) => { + return { + id: option.id, + accepted: option.accepted, + optionTitle: option.optionTitle, + optionSubTitle: option.optionSubTitle, + voteScore: option.voteScore, + questionId: option.questionId, + registrationId: option.registrationId, + user: { + username: option.user?.username, + group: option.user?.usersToGroups[0]?.group, + }, + createdAt: option.createdAt, + updatedAt: option.updatedAt, + }; + }), + }; + }), }; + + return out; } diff --git a/src/services/forumQuestions.spec.ts b/src/services/forumQuestions.spec.ts index 85a10725..9599411e 100644 --- a/src/services/forumQuestions.spec.ts +++ b/src/services/forumQuestions.spec.ts @@ -1,7 +1,7 @@ import { availableHearts } from './forumQuestions'; // Test availableHearts function -describe('availableHearts function', () => { +describe('service: forumQuestions', () => { test('calculates available hearts correctly', () => { const numProposals = 2; const baseNumerator = 4; diff --git a/src/services/registrations.spec.ts b/src/services/registrations.spec.ts index 2a68e752..ce785ceb 100644 --- a/src/services/registrations.spec.ts +++ b/src/services/registrations.spec.ts @@ -10,7 +10,7 @@ import { cleanup, seed } from '../utils/db/seed'; const DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432'; -describe('sendRegistrationData function', () => { +describe('service: registrations', () => { let dbPool: PostgresJsDatabase; let dbConnection: postgres.Sql>; let testData: z.infer; diff --git a/src/services/registrations.ts b/src/services/registrations.ts index 0d6d7128..c291fcc8 100644 --- a/src/services/registrations.ts +++ b/src/services/registrations.ts @@ -78,6 +78,25 @@ export function getRegistration(dbPool: PostgresJsDatabase) { }; } +export function getUserRegistrations(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + // parse input + const userId = req.session.userId; + + try { + const out = await dbPool + .select() + .from(db.registrations) + .where(eq(db.registrations.userId, userId)); + + return res.json({ data: out }); + } catch (e) { + console.log('error getting user registrations ' + e); + return res.sendStatus(500); + } + }; +} + export async function sendRegistrationData( dbPool: PostgresJsDatabase, data: z.infer, diff --git a/src/services/resultsPage.spec.ts b/src/services/resultsPage.spec.ts deleted file mode 100644 index c9bc5f86..00000000 --- a/src/services/resultsPage.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import { getResultStatistics } from './resultsPage'; -import * as db from '../db'; -import postgres from 'postgres'; -import { createDbPool } from '../utils/db/createDbPool'; -import { runMigrations } from '../utils/db/runMigrations'; -import { cleanup, seed } from '../utils/db/seed'; -import { Request, Response } from 'express'; - -const DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432'; - -describe('getResultStatistics endpoint', () => { - let dbPool: PostgresJsDatabase; - let req: Partial; - let res: Partial; - let dbConnection: postgres.Sql>; - - beforeAll(async () => { - const initDb = createDbPool(DB_CONNECTION_URL, { max: 1 }); - await runMigrations(DB_CONNECTION_URL); - dbPool = initDb.dbPool; - - // Seed the database - await seed(dbPool); - - // Initialize req and res objects - req = {}; - res = {}; - - // Initialize dbConnection - dbConnection = initDb.connection; - }); - - test('should return aggregated statistics when all queries return valid data', async () => { - // Mock forumQuestionId in req.params - req.params = { forumQuestionId: '3eac2a7b-a20d-4157-9855-ad7a65a5a731' }; - - // Mock res.json and res.status to capture the response - res.json = jest.fn(); - res.status = jest.fn().mockReturnValue(res); - - // Call getResultStatistics - await getResultStatistics(dbPool)(req as Request, res as Response); - - // Assertions for response - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalled(); - }); - - afterAll(async () => { - await cleanup(dbPool); - await dbConnection.end(); - }); -}); diff --git a/src/services/resultsPage.ts b/src/services/resultsPage.ts deleted file mode 100644 index 9759da7b..00000000 --- a/src/services/resultsPage.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import * as db from '../db'; -import type { Request, Response } from 'express'; -import { sql } from 'drizzle-orm'; - -/** - * Retrieves aggregate statistics for the results page, including total number of proposals, - * total allocated hearts, number of participants, number of groups, and individual results. - * @param {PostgresJsDatabase} dbPool - The database connection pool. - * @returns {Promise} A promise that resolves with the aggregated statistics JSON response. - */ -export function getResultStatistics(dbPool: PostgresJsDatabase) { - return async function (req: Request, res: Response) { - try { - const forumQuestionId = req.params.forumQuestionId; - - // Execute all queries concurrently - const [ - queryResultNumProposals, - queryResultAllocatedHearts, - queryNumOfParticipants, - queryNumOfGroups, - queryIndivStatistics, - ] = await Promise.all([ - // Get total number of proposals - dbPool.execute<{ numProposals: number }>( - sql.raw(` - SELECT count("id") AS "numProposals" - FROM question_options - WHERE question_id = '${forumQuestionId}' - AND accepted = TRUE - `), - ), - - // Get total allocated hearts - dbPool.execute<{ sumNumOfHearts: number }>( - sql.raw(` - SELECT sum(num_of_votes) AS "sumNumOfHearts" - FROM ( - SELECT user_id, num_of_votes, updated_at, - ROW_NUMBER() OVER (PARTITION BY user_id, option_id ORDER BY updated_at DESC) as row_num - FROM votes - WHERE question_id = '${forumQuestionId}' - ) AS ranked - WHERE row_num = 1 - `), - ), - - // Get number of Participants - dbPool.execute<{ numOfParticipants: number }>( - sql.raw(` - SELECT count(DISTINCT user_id) AS "numOfParticipants" - FROM votes - WHERE question_id = '${forumQuestionId}' - `), - ), - - // Get number of Groups - dbPool.execute<{ numOfGroups: number }>( - sql.raw(` - WITH votes_users AS ( - SELECT DISTINCT user_id - FROM votes - WHERE question_id = '${forumQuestionId}' - ) - - SELECT count(DISTINCT group_id) AS "numOfGroups" - FROM users_to_groups - WHERE user_id IN (SELECT user_id FROM votes_users) - `), - ), - - // Get individual results - dbPool.execute<{ - optionId: string; - optionTitle: string; - optionSubTitle: string; - pluralityScore: number; - distinctUsers: number; - allocatedHearts: number; - distinctGroups: number; - listOfGroupNames: string[]; - }>( - sql.raw(` - WITH distinct_voters_by_option AS ( - SELECT option_id AS "optionId", count(DISTINCT user_id) AS "distinctUsers" - FROM votes - WHERE question_id = '${forumQuestionId}' - GROUP BY option_id - ), - - plural_score_and_title AS ( - SELECT "id" AS "optionId", "option_title" AS "optionTitle", "option_sub_title" AS "optionSubTitle", vote_score AS "pluralityScore" - FROM question_options - WHERE question_id = '${forumQuestionId}' - AND accepted = TRUE -- makes sure to only expose data of accepted options - ), - - allocated_hearts AS ( - SELECT option_id AS "optionId", sum(num_of_votes) AS "allocatedHearts" - FROM ( - SELECT user_id, option_id, num_of_votes, updated_at, - ROW_NUMBER() OVER (PARTITION BY user_id, option_id ORDER BY updated_at DESC) as row_num - FROM votes - WHERE question_id = '${forumQuestionId}' - ) AS ranked - WHERE row_num = 1 - GROUP BY option_id - ), - - /* Query distinct groups and group names by option id */ - user_group_name AS ( - SELECT users_to_groups."user_id", users_to_groups."group_id", groups."name" - FROM users_to_groups - LEFT JOIN groups - ON users_to_groups."group_id" = groups."id" - ), - - option_user AS ( - SELECT option_id, user_id - FROM votes - WHERE question_id = '${forumQuestionId}' - GROUP BY option_id, user_id - ), - - option_user_group_name AS ( - SELECT option_user."user_id", option_user."option_id", - user_group_name."group_id", user_group_name."name" - FROM option_user - LEFT JOIN user_group_name - ON option_user."user_id" = user_group_name."user_id" - ), - - option_distinct_group_name AS ( - SELECT option_id AS "optionId", count(DISTINCT group_id) AS "distinctGroups", - STRING_TO_ARRAY(STRING_AGG(DISTINCT name, ','), ',') AS "listOfGroupNames" - FROM option_user_group_name - GROUP BY option_id - ), - - /* Aggregated results */ - merged_result AS ( - SELECT id_title_score."optionId", - id_title_score."optionTitle", - id_title_score."optionSubTitle", - id_title_score."pluralityScore", - distinct_users."distinctUsers", - hearts."allocatedHearts", - group_count_names."distinctGroups", - group_count_names."listOfGroupNames" - FROM plural_score_and_title AS id_title_score - LEFT JOIN distinct_voters_by_option AS distinct_users - ON id_title_score."optionId" = distinct_users."optionId" - LEFT JOIN allocated_hearts AS hearts - ON id_title_score."optionId" = hearts."optionId" - LEFT JOIN option_distinct_group_name AS group_count_names - ON id_title_score."optionId" = group_count_names."optionId" - ) - - SELECT * - FROM merged_result - `), - ), - ]); - - const numProposals = queryResultNumProposals[0]?.numProposals; - const sumNumOfHearts = queryResultAllocatedHearts[0]?.sumNumOfHearts; - const numOfParticipants = queryNumOfParticipants[0]?.numOfParticipants; - const numOfGroups = queryNumOfGroups[0]?.numOfGroups; - const indivStats: Record< - string, - { - optionTitle: string; - optionSubTitle: string; - pluralityScore: number; - distinctUsers: number; - allocatedHearts: number; - distinctGroups: number; - listOfGroupNames: string[]; - } - > = {}; - - // Loop through each row in queryIndivStatistics - queryIndivStatistics.forEach((row) => { - const { - optionId: indivOptionId, - optionTitle: indivOptionTitle, - optionSubTitle: indivOptionSubTitle, - pluralityScore: indivPluralityScore, - distinctUsers: indivDistinctUsers, - allocatedHearts: indivAllocatedHearts, - distinctGroups: indivdistinctGroups, - listOfGroupNames: indivlistOfGroupNames, - } = row; - - indivStats[indivOptionId] = { - optionTitle: indivOptionTitle || 'No Title Provided', - optionSubTitle: indivOptionSubTitle || '', - pluralityScore: indivPluralityScore || 0, - distinctUsers: indivDistinctUsers || 0, - allocatedHearts: indivAllocatedHearts || 0, - distinctGroups: indivdistinctGroups || 0, - listOfGroupNames: indivlistOfGroupNames || [], - }; - }); - - const responseData = { - numProposals: numProposals || 0, - sumNumOfHearts: sumNumOfHearts || 0, - numOfParticipants: numOfParticipants || 0, - numOfGroups: numOfGroups || 0, - optionStats: indivStats, - }; - - return res.status(200).json({ data: responseData }); - } catch (error) { - console.error('Error in getResultStatistics:', error); - return res.status(500).json({ error: 'Internal Server Error' }); - } - }; -} diff --git a/src/services/statistics.spec.ts b/src/services/statistics.spec.ts new file mode 100644 index 00000000..8e54ccd9 --- /dev/null +++ b/src/services/statistics.spec.ts @@ -0,0 +1,101 @@ +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 { insertVotesSchema } from '../types'; +import { cleanup, seed } from '../utils/db/seed'; +import { z } from 'zod'; +import { executeResultQueries } from './statistics'; +import { eq } from 'drizzle-orm'; + +const DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432'; + +describe('service: statistics', () => { + let dbPool: PostgresJsDatabase; + let dbConnection: postgres.Sql>; + let userTestData: z.infer; + let otherUserTestData: z.infer; + let cycle: db.Cycle | undefined; + let questionOption: db.QuestionOption | undefined; + let forumQuestion: db.ForumQuestion | undefined; + let user: db.User | undefined; + let otherUser: db.User | undefined; + + beforeAll(async () => { + const initDb = createDbPool(DB_CONNECTION_URL, { max: 1 }); + await runMigrations(DB_CONNECTION_URL); + dbPool = initDb.dbPool; + dbConnection = initDb.connection; + // seed + const { users, questionOptions, forumQuestions, cycles } = await seed(dbPool); + // Insert registration fields for the user + questionOption = questionOptions[0]; + forumQuestion = forumQuestions[0]; + user = users[0]; + otherUser = users[1]; + cycle = cycles[0]; + userTestData = { + numOfVotes: 2, + optionId: questionOption?.id ?? '', + questionId: forumQuestion?.id ?? '', + userId: user?.id ?? '', + }; + otherUserTestData = { + numOfVotes: 2, + optionId: questionOption?.id ?? '', + questionId: forumQuestion?.id ?? '', + userId: otherUser?.id ?? '', + }; + + // Add additional data to the Db + await dbPool.insert(db.votes).values(userTestData); + await dbPool.insert(db.votes).values(otherUserTestData); + }); + + test('should return aggregated statistics when all queries return valid data', async () => { + const questionId = forumQuestion!.id; + + // Call getResultStatistics with the required parameters + const result = await executeResultQueries(questionId, dbPool); + + // Test aggregate result statistics + expect(result).toBeDefined(); + expect(result.numProposals).toEqual(2); + expect(result.sumNumOfHearts).toEqual(4); + expect(result.numOfParticipants).toEqual(2); + + // Test option stats + expect(result.optionStats).toBeDefined(); + expect(Object.keys(result.optionStats)).toHaveLength(2); + + for (const optionId in result.optionStats) { + const optionStat = result.optionStats[optionId]; + expect(optionStat).toBeDefined(); + expect(optionStat?.optionTitle).toBeDefined(); + expect(optionStat?.optionSubTitle).toBeDefined(); + expect(optionStat?.pluralityScore).toBeDefined(); + expect(optionStat?.distinctUsers).toBeDefined(); + expect(optionStat?.allocatedHearts).toBeDefined(); + expect(optionStat?.distinctGroups).toBeDefined(); + expect(optionStat?.listOfGroupNames).toBeDefined(); + + // Add assertions for distinct users and allocated hearts + if (optionId === questionOption?.id) { + // Assuming this option belongs to the user + expect(optionStat?.distinctUsers).toEqual(2); + expect(optionStat?.allocatedHearts).toEqual(4); + expect(optionStat?.distinctGroups).toEqual(1); + const listOfGroupNames = optionStat?.listOfGroupNames; + // Check if the array is not empty + expect(listOfGroupNames).toBeDefined(); + expect(listOfGroupNames?.length).toBeGreaterThan(0); + } + } + }); + + afterAll(async () => { + await cleanup(dbPool); + await dbConnection.end(); + }); +}); diff --git a/src/services/statistics.ts b/src/services/statistics.ts new file mode 100644 index 00000000..7811367c --- /dev/null +++ b/src/services/statistics.ts @@ -0,0 +1,271 @@ +import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import * as db from '../db'; +import type { Request, Response } from 'express'; +import { sql } from 'drizzle-orm'; + +type ResultData = { + numProposals: number; + sumNumOfHearts: number; + numOfParticipants: number; + numOfGroups: number; + optionStats: Record< + string, + { + optionTitle: string; + optionSubTitle: string; + pluralityScore: string; + distinctUsers: number; + allocatedHearts: number; + distinctGroups: number; + listOfGroupNames: string[]; + } + >; +}; + +/** + * Retrieves result statistics for a specific forum question from the database. + * + * @param {PostgresJsDatabase} dbPool - The PostgreSQL database pool instance. + * @returns {Function} - An Express middleware function handling the request to retrieve result statistics. + * @param {Request} req - The Express request object. + * @param {Response} res - The Express response object. + * @returns {Promise} - A promise that resolves with the Express response containing the result statistics data. + */ +export function getResultStatistics(dbPool: PostgresJsDatabase) { + return async function (req: Request, res: Response) { + try { + const forumQuestionId = req.params.forumQuestionId; + + // Check if forumQuestionId is provided + if (!forumQuestionId) { + return res.status(400).json({ error: 'Missing forumQuestionId parameter' }); + } + + // Execute queries + const responseData = await executeResultQueries(forumQuestionId, dbPool); + + // Send response + return res.status(200).json({ data: responseData }); + } catch (error) { + console.error('Error in getResultStatistics:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } + }; +} + +/** + * Executes multiple queries concurrently to retrieve statistics related to a forum question from the database. + * + * @param {string | undefined} forumQuestionId - The ID of the forum question for which statistics are to be retrieved. + * @param {PostgresJsDatabase} dbPool - The PostgreSQL database pool instance. + * @returns {Promise} - A promise resolving to an object containing various statistics related to the forum question. + */ +export async function executeResultQueries( + forumQuestionId: string | undefined, + dbPool: PostgresJsDatabase, +): Promise { + try { + // Execute all queries concurrently + const [ + queryResultNumProposals, + queryResultAllocatedHearts, + queryNumOfParticipants, + queryNumOfGroups, + queryIndivStatistics, + ] = await Promise.all([ + // Get total number of proposals + dbPool.execute<{ numProposals: number }>( + sql.raw(` + SELECT count("id")::int AS "numProposals" + FROM question_options + WHERE question_id = '${forumQuestionId}' + AND accepted = TRUE + `), + ), + + // Get total allocated hearts + dbPool.execute<{ sumNumOfHearts: number }>( + sql.raw(` + SELECT sum(num_of_votes)::int AS "sumNumOfHearts" + FROM ( + SELECT user_id, num_of_votes, updated_at, + ROW_NUMBER() OVER (PARTITION BY user_id, option_id ORDER BY updated_at DESC) as row_num + FROM votes + WHERE question_id = '${forumQuestionId}' + ) AS ranked + WHERE row_num = 1 + `), + ), + + // Get number of Participants + dbPool.execute<{ numOfParticipants: number }>( + sql.raw(` + SELECT count(DISTINCT user_id)::int AS "numOfParticipants" + FROM votes + WHERE question_id = '${forumQuestionId}' + `), + ), + + // Get number of Groups + dbPool.execute<{ numOfGroups: number }>( + sql.raw(` + WITH votes_users AS ( + SELECT DISTINCT user_id + FROM votes + WHERE question_id = '${forumQuestionId}' + ) + + SELECT count(DISTINCT group_id)::int AS "numOfGroups" + FROM users_to_groups + WHERE user_id IN (SELECT user_id FROM votes_users) + `), + ), + + // Get individual results + dbPool.execute<{ + optionId: string; + optionTitle: string; + optionSubTitle: string; + pluralityScore: string; + distinctUsers: number; + allocatedHearts: number; + distinctGroups: number; + listOfGroupNames: string[]; + }>( + sql.raw(` + WITH distinct_voters_by_option AS ( + SELECT option_id AS "optionId", count(DISTINCT user_id)::int AS "distinctUsers" + FROM votes + WHERE question_id = '${forumQuestionId}' + GROUP BY option_id + ), + + plural_score_and_title AS ( + SELECT "id" AS "optionId", "option_title" AS "optionTitle", "option_sub_title" AS "optionSubTitle", vote_score AS "pluralityScore" + FROM question_options + WHERE question_id = '${forumQuestionId}' + AND accepted = TRUE -- makes sure to only expose data of accepted options + ), + + allocated_hearts AS ( + SELECT option_id AS "optionId", sum(num_of_votes)::int AS "allocatedHearts" + FROM ( + SELECT user_id, option_id, num_of_votes, updated_at, + ROW_NUMBER() OVER (PARTITION BY user_id, option_id ORDER BY updated_at DESC) as row_num + FROM votes + WHERE question_id = '${forumQuestionId}' + ) AS ranked + WHERE row_num = 1 + GROUP BY option_id + ), + + /* Query distinct groups and group names by option id */ + user_group_name AS ( + SELECT users_to_groups."user_id", users_to_groups."group_id", groups."name" + FROM users_to_groups + LEFT JOIN groups + ON users_to_groups."group_id" = groups."id" + ), + + option_user AS ( + SELECT option_id, user_id + FROM votes + WHERE question_id = '${forumQuestionId}' + GROUP BY option_id, user_id + ), + + option_user_group_name AS ( + SELECT option_user."user_id", option_user."option_id", + user_group_name."group_id", user_group_name."name" + FROM option_user + LEFT JOIN user_group_name + ON option_user."user_id" = user_group_name."user_id" + ), + + option_distinct_group_name AS ( + SELECT option_id AS "optionId", count(DISTINCT group_id)::int AS "distinctGroups", + STRING_TO_ARRAY(STRING_AGG(DISTINCT name, ';'), ';') AS "listOfGroupNames" + FROM option_user_group_name + GROUP BY option_id + ), + + /* Aggregated results */ + merged_result AS ( + SELECT id_title_score."optionId", + id_title_score."optionTitle", + id_title_score."optionSubTitle", + id_title_score."pluralityScore", + distinct_users."distinctUsers", + hearts."allocatedHearts", + group_count_names."distinctGroups", + group_count_names."listOfGroupNames" + FROM plural_score_and_title AS id_title_score + LEFT JOIN distinct_voters_by_option AS distinct_users + ON id_title_score."optionId" = distinct_users."optionId" + LEFT JOIN allocated_hearts AS hearts + ON id_title_score."optionId" = hearts."optionId" + LEFT JOIN option_distinct_group_name AS group_count_names + ON id_title_score."optionId" = group_count_names."optionId" + ) + + SELECT * + FROM merged_result + `), + ), + ]); + + const numProposals = queryResultNumProposals[0]?.numProposals; + const sumNumOfHearts = queryResultAllocatedHearts[0]?.sumNumOfHearts; + const numOfParticipants = queryNumOfParticipants[0]?.numOfParticipants; + const numOfGroups = queryNumOfGroups[0]?.numOfGroups; + const indivStats: Record< + string, + { + optionTitle: string; + optionSubTitle: string; + pluralityScore: string; + distinctUsers: number; + allocatedHearts: number; + distinctGroups: number; + listOfGroupNames: string[]; + } + > = {}; + + // Loop through each row in queryIndivStatistics + queryIndivStatistics.forEach((row) => { + const { + optionId: indivOptionId, + optionTitle: indivOptionTitle, + optionSubTitle: indivOptionSubTitle, + pluralityScore: indivPluralityScore, + distinctUsers: indivDistinctUsers, + allocatedHearts: indivAllocatedHearts, + distinctGroups: indivdistinctGroups, + listOfGroupNames: indivlistOfGroupNames, + } = row; + + indivStats[indivOptionId] = { + optionTitle: indivOptionTitle || 'No Title Provided', + optionSubTitle: indivOptionSubTitle || '', + pluralityScore: indivPluralityScore || '0.0', + distinctUsers: indivDistinctUsers || 0, + allocatedHearts: indivAllocatedHearts || 0, + distinctGroups: indivdistinctGroups || 0, + listOfGroupNames: indivlistOfGroupNames || [], + }; + }); + + const responseData = { + numProposals: numProposals || 0, + sumNumOfHearts: sumNumOfHearts || 0, + numOfParticipants: numOfParticipants || 0, + numOfGroups: numOfGroups || 0, + optionStats: indivStats, + }; + + return responseData; + } catch (error) { + console.error('Error in executeQueries:', error); + throw new Error('Error executing database queries'); + } +} diff --git a/src/services/users.ts b/src/services/users.ts index f9f9e67b..5f68ce36 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -81,7 +81,6 @@ export function updateUser(dbPool: PostgresJsDatabase) { .update(db.users) .set({ email: body.data.email, - emailNotification: body.data.emailNotification, username: body.data.username, name: body.data.name, updatedAt: new Date(),