From e8aa5fc4a0fe2fd296ec4a3717f29cf9f50e2d71 Mon Sep 17 00:00:00 2001 From: Martin Benedikt Busch <43137759+MartinBenediktBusch@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:45:39 +0200 Subject: [PATCH 01/14] add multiple voting models (#438) * add vote model to forum questions * add voting models * add COCM model as default --- migrations/0026_redundant_galactus.sql | 1 + migrations/meta/0026_snapshot.json | 1647 ++++++++++++++++++++++++ migrations/meta/_journal.json | 7 + src/db/forum-questions.ts | 1 + src/services/votes.spec.ts | 4 +- src/services/votes.ts | 92 +- src/utils/db/seed-data-generators.ts | 6 +- src/utils/db/seed.ts | 3 +- 8 files changed, 1735 insertions(+), 26 deletions(-) create mode 100644 migrations/0026_redundant_galactus.sql create mode 100644 migrations/meta/0026_snapshot.json diff --git a/migrations/0026_redundant_galactus.sql b/migrations/0026_redundant_galactus.sql new file mode 100644 index 00000000..da5cc307 --- /dev/null +++ b/migrations/0026_redundant_galactus.sql @@ -0,0 +1 @@ +ALTER TABLE "forum_questions" ADD COLUMN "vote_model" varchar(256) DEFAULT 'COCM' NOT NULL; \ No newline at end of file diff --git a/migrations/meta/0026_snapshot.json b/migrations/meta/0026_snapshot.json new file mode 100644 index 00000000..dd88ba47 --- /dev/null +++ b/migrations/meta/0026_snapshot.json @@ -0,0 +1,1647 @@ +{ + "id": "3e25dac0-f63a-429e-beec-e0b014693359", + "prevId": "d5949f15-27cf-4e59-8b74-fc60826fef74", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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 + }, + "require_approval": { + "name": "require_approval", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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": {} + }, + "public.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" + ] + } + } + }, + "public.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 + }, + "vote_model": { + "name": "vote_model", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "default": "'COCM'" + }, + "show_score": { + "name": "show_score", + "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": { + "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": {} + }, + "public.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 + }, + "required": { + "name": "required", + "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": {} + }, + "public.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": 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": { + "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" + ] + } + } + }, + "public.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" + ] + } + } + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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'" + }, + "funding_request": { + "name": "funding_request", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "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": {} + }, + "public.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": {} + }, + "public.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 + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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" + ] + } + } + }, + "public.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": {} + }, + "public.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 f7d0afd6..06671681 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -183,6 +183,13 @@ "when": 1718889966152, "tag": "0025_narrow_warhawk", "breakpoints": true + }, + { + "idx": 26, + "version": "6", + "when": 1719999199277, + "tag": "0026_redundant_galactus", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/forum-questions.ts b/src/db/forum-questions.ts index f5d5f876..031d1200 100644 --- a/src/db/forum-questions.ts +++ b/src/db/forum-questions.ts @@ -11,6 +11,7 @@ export const forumQuestions = pgTable('forum_questions', { .notNull(), questionTitle: varchar('question_title', { length: 256 }).notNull(), questionSubTitle: varchar('question_sub_title', { length: 256 }), + voteModel: varchar('vote_model', { length: 256 }).notNull().default('COCM'), showScore: boolean('show_score').default(false), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), diff --git a/src/services/votes.spec.ts b/src/services/votes.spec.ts index 4a71b852..d2b6b426 100644 --- a/src/services/votes.spec.ts +++ b/src/services/votes.spec.ts @@ -13,7 +13,7 @@ import { calculatePluralScore, calculateQuadraticScore, updateVoteScoreInDatabase, - updateVoteScore, + updateVoteScorePlural, userCanVote, } from './votes'; import { eq } from 'drizzle-orm'; @@ -334,7 +334,7 @@ describe('service: votes', () => { test('full integration test of the update vote functionality', async () => { // Test that the plurality score is correct if both users are in the same group - const score = await updateVoteScore(dbPool, questionOption?.id ?? ''); + const score = await updateVoteScorePlural(dbPool, questionOption?.id ?? ''); // sqrt of 2 because the two users are in the same group // voting for the same option with 1 vote each expect(score).toBe(Math.sqrt(2)); diff --git a/src/services/votes.ts b/src/services/votes.ts index 2700c117..416f7d91 100644 --- a/src/services/votes.ts +++ b/src/services/votes.ts @@ -10,33 +10,71 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; /** * Saves votes submitted by a user. + * + * This function validates and saves each vote provided in the `data` array for the specified `userId`. + * It then updates the vote count for each option based on the vote model associated with the question. + * + * @param dbPool - The database connection pool. + * @param data - An array of objects containing `optionId` and `numOfVotes` properties. + * @param userId - The ID of the user submitting the votes. + * @returns A promise that resolves to an object containing `data` (an array of saved votes) and `errors` (an array of error messages). */ export async function saveVotes( dbPool: NodePgDatabase, data: { optionId: string; numOfVotes: number }[], userId: string, ): Promise<{ data: db.Vote[]; errors: string[] }> { - const out: db.Vote[] = []; + const voteData: db.Vote[] = []; const errors: string[] = []; for (const vote of data) { const { data, error } = await validateAndSaveVote(dbPool, vote, userId); if (data) { - out.push(data); + voteData.push(data); } if (error) { errors.push(error); } } - const uniqueOptionIds = new Set(out.map((vote) => vote.optionId)); + const queryQuestionOption = await dbPool.query.questionOptions.findFirst({ + where: eq(db.questionOptions.id, voteData[0]!.optionId), + }); + + if (!queryQuestionOption) { + errors.push('No option found for the provided optionId'); + return { data: voteData, errors }; + } + + const queryForumQuestion = await dbPool.query.forumQuestions.findFirst({ + where: eq(db.forumQuestions.id, queryQuestionOption!.questionId), + }); - // Update the vote count for each option - for (const optionId of uniqueOptionIds) { - await updateVoteScore(dbPool, optionId); + if (!queryForumQuestion) { + errors.push('No question found for the provided questionId'); + return { data: voteData, errors }; } - return { data: out, errors }; + // Define available voting models + const voteModelUpdateFunctions = { + COCM: updateVoteScorePlural, + QV: updateVoteScoreQuadratic, + }; + + const updateFunction = + voteModelUpdateFunctions[ + queryForumQuestion?.voteModel as keyof typeof voteModelUpdateFunctions + ]; + + const uniqueOptionIds = voteData.map((vote) => vote.optionId); + + if (!updateFunction) { + errors.push('Unsupported vote model: ' + queryForumQuestion.voteModel); + } else { + await Promise.all(uniqueOptionIds.map((optionId) => updateFunction(dbPool, optionId))); + } + + return { data: voteData, errors }; } /** @@ -178,7 +216,7 @@ export async function updateVoteScoreInDatabase( } /** - * Updates the vote score for a specific option in the database. + * Updates the vote score for a specific option in the database according to the plural voting model. * * This function queries vote and multiplier data from the database, * combines them, calculates the score using plural voting, updates @@ -187,17 +225,15 @@ export async function updateVoteScoreInDatabase( * @param { NodePgDatabase} dbPool - The database connection pool. * @param {string} optionId - The ID of the option for which to update the vote score. */ -export async function updateVoteScore( +export async function updateVoteScorePlural( dbPool: NodePgDatabase, optionId: string, ): Promise { - // Query vote data and multiplier from the database + // Query and transform vote data const voteArray = await queryVoteData(dbPool, optionId); - - // Transform data const votesDictionary = await numOfVotesDictionary(voteArray); - // Query question Id + // Query group data, grouping dimensions, and calculate the score const queryQuestionId = await dbPool .select({ questionId: db.questionOptions.questionId, @@ -205,19 +241,33 @@ export async function updateVoteScore( .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, groupCategories ?? []); - - // Perform plural voting calculation const score = await calculatePluralScore(groupArray, votesDictionary); - // Perform quadratic score calculation - // const score = await calculateQuadraticScore(votesDictionary); + await updateVoteScoreInDatabase(dbPool, optionId, score); + + return score; +} + +/** + * Updates the vote score for a specific option in the database according to the quadratic voting model. + * + * This function queries vote and multiplier data from the database, + * combines them, calculates the score using quadratic voting, updates + * the vote score in the database, and returns the calculated score. + * + * @param { NodePgDatabase} dbPool - The database connection pool. + * @param {string} optionId - The ID of the option for which to update the vote score. + */ +export async function updateVoteScoreQuadratic( + dbPool: NodePgDatabase, + optionId: string, +): Promise { + const voteArray = await queryVoteData(dbPool, optionId); + const votesDictionary = await numOfVotesDictionary(voteArray); + const score = await calculateQuadraticScore(votesDictionary); - // Update vote score in the database await updateVoteScoreInDatabase(dbPool, optionId, score); return score; diff --git a/src/utils/db/seed-data-generators.ts b/src/utils/db/seed-data-generators.ts index e642c232..db90ae30 100644 --- a/src/utils/db/seed-data-generators.ts +++ b/src/utils/db/seed-data-generators.ts @@ -24,7 +24,7 @@ export type RegistrationFieldOptionData = Pick< RegistrationFieldOption, 'registrationFieldId' | 'value' >; -export type ForumQuestionData = Pick; +export type ForumQuestionData = Pick; export type QuestionOptionData = Pick; export type GroupCategoryData = Pick< GroupCategory, @@ -93,10 +93,12 @@ export function generateRegistrationFieldOptionsData( export function generateForumQuestionData( cycleId: string, questionTitles: string[], + voteModels: string[], ): ForumQuestionData[] { - return questionTitles.map((questionTitle) => ({ + return questionTitles.map((questionTitle, index) => ({ cycleId, questionTitle, + voteModel: voteModels[index] ?? 'COCM', })); } diff --git a/src/utils/db/seed.ts b/src/utils/db/seed.ts index 39c43b62..3614998c 100644 --- a/src/utils/db/seed.ts +++ b/src/utils/db/seed.ts @@ -44,7 +44,7 @@ async function seed(dbPool: NodePgDatabase) { ); const forumQuestions = await createForumQuestions( dbPool, - generateForumQuestionData(cycles[0]!.id, ['Question One', 'Question Two']), + generateForumQuestionData(cycles[0]!.id, ['Question One', 'Question Two'], ['COCM', 'QV']), ); const questionOptions = await createQuestionOptions( dbPool, @@ -265,6 +265,7 @@ async function createForumQuestions( .values({ cycleId: questionData.cycleId, questionTitle: questionData.questionTitle, + voteModel: questionData.voteModel, }) .returning(); From 7f9c6768d64a45c90bb9125ec72bd29303fb8205 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Wed, 3 Jul 2024 20:50:38 +0100 Subject: [PATCH 02/14] rename db tables to have more consistency (#434) * rename db tables to have more consistency * fix renaming variables * fix tests * update property names * drop db changes * create new migration starting from vote model changes * update github actions pnpm setup to v4 to stop breaking actions issue * fix tests --------- Co-authored-by: Martin Benedikt Busch --- .github/workflows/check-formatting.yml | 4 +- .github/workflows/deploy-development.yml | 2 +- .github/workflows/deploy-production.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- migrations/0027_stormy_silverclaw.sql | 70 + migrations/meta/0027_snapshot.json | 1647 +++++++++++++++++ migrations/meta/_journal.json | 7 + src/db/comments.ts | 10 +- src/db/cycles.ts | 4 +- src/db/index.ts | 4 +- src/db/{question-options.ts => options.ts} | 24 +- src/db/questions-to-group-categories.ts | 8 +- src/db/{forum-questions.ts => questions.ts} | 16 +- src/db/users.ts | 4 +- src/db/votes.ts | 16 +- src/handlers/comments.ts | 2 +- src/handlers/cycles.ts | 6 +- src/handlers/events.ts | 6 +- src/handlers/options.ts | 6 +- .../{forum-questions.ts => questions.ts} | 0 src/handlers/users.ts | 6 +- src/routers/api.ts | 4 +- .../{forum-questions.ts => questions.ts} | 2 +- src/services/comments.spec.ts | 6 +- src/services/comments.ts | 26 +- src/services/cycles.spec.ts | 4 +- src/services/cycles.ts | 22 +- src/services/funding-mechanism.ts | 10 +- src/services/statistics.spec.ts | 8 +- src/services/statistics.ts | 30 +- src/services/votes.spec.ts | 12 +- src/services/votes.ts | 26 +- src/utils/db/seed-data-generators.ts | 14 +- src/utils/db/seed.ts | 12 +- 35 files changed, 1873 insertions(+), 151 deletions(-) create mode 100644 migrations/0027_stormy_silverclaw.sql create mode 100644 migrations/meta/0027_snapshot.json rename src/db/{question-options.ts => options.ts} (61%) rename src/db/{forum-questions.ts => questions.ts} (60%) rename src/handlers/{forum-questions.ts => questions.ts} (100%) rename src/routers/{forum-questions.ts => questions.ts} (95%) diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index a7d05777..56a739ce 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8 - name: Setup Node.js environment @@ -33,4 +33,4 @@ jobs: run: pnpm install - name: Review formatting - run: pnpm format \ No newline at end of file + run: pnpm format diff --git a/.github/workflows/deploy-development.yml b/.github/workflows/deploy-development.yml index 4c00df52..c0d1456e 100644 --- a/.github/workflows/deploy-development.yml +++ b/.github/workflows/deploy-development.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8 - name: Setup Node.js environment diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 4759044f..f7226f4e 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8 - name: Setup Node.js environment diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 579c3eee..ff1afcd7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8 - name: Setup Node.js environment diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3eca7847..fdc2e002 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8 - name: Setup Node.js environment diff --git a/migrations/0027_stormy_silverclaw.sql b/migrations/0027_stormy_silverclaw.sql new file mode 100644 index 00000000..9e028b22 --- /dev/null +++ b/migrations/0027_stormy_silverclaw.sql @@ -0,0 +1,70 @@ +ALTER TABLE "forum_questions" RENAME TO "questions";--> statement-breakpoint +ALTER TABLE "question_options" RENAME TO "options";--> statement-breakpoint +ALTER TABLE "comments" RENAME COLUMN "question_option_id" TO "option_id";--> statement-breakpoint +ALTER TABLE "questions" RENAME COLUMN "question_title" TO "title";--> statement-breakpoint +ALTER TABLE "questions" RENAME COLUMN "question_sub_title" TO "sub_title";--> statement-breakpoint +ALTER TABLE "options" RENAME COLUMN "option_title" TO "title";--> statement-breakpoint +ALTER TABLE "options" RENAME COLUMN "option_sub_title" TO "sub_title";--> statement-breakpoint +ALTER TABLE "comments" DROP CONSTRAINT "comments_question_option_id_question_options_id_fk"; +--> statement-breakpoint +ALTER TABLE "questions" DROP CONSTRAINT "forum_questions_cycle_id_cycles_id_fk"; +--> statement-breakpoint +ALTER TABLE "options" DROP CONSTRAINT "question_options_user_id_users_id_fk"; +--> statement-breakpoint +ALTER TABLE "options" DROP CONSTRAINT "question_options_registration_id_registrations_id_fk"; +--> statement-breakpoint +ALTER TABLE "options" DROP CONSTRAINT "question_options_question_id_forum_questions_id_fk"; +--> statement-breakpoint +ALTER TABLE "votes" DROP CONSTRAINT "votes_option_id_question_options_id_fk"; +--> statement-breakpoint +ALTER TABLE "votes" DROP CONSTRAINT "votes_question_id_forum_questions_id_fk"; +--> statement-breakpoint +ALTER TABLE "questions_to_group_categories" DROP CONSTRAINT "questions_to_group_categories_question_id_forum_questions_id_fk"; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "comments" ADD CONSTRAINT "comments_option_id_options_id_fk" FOREIGN KEY ("option_id") REFERENCES "public"."options"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "questions" ADD CONSTRAINT "questions_cycle_id_cycles_id_fk" FOREIGN KEY ("cycle_id") REFERENCES "public"."cycles"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "options" ADD CONSTRAINT "options_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "options" ADD CONSTRAINT "options_registration_id_registrations_id_fk" FOREIGN KEY ("registration_id") REFERENCES "public"."registrations"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "options" ADD CONSTRAINT "options_question_id_questions_id_fk" FOREIGN KEY ("question_id") REFERENCES "public"."questions"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "votes" ADD CONSTRAINT "votes_option_id_options_id_fk" FOREIGN KEY ("option_id") REFERENCES "public"."options"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "votes" ADD CONSTRAINT "votes_question_id_questions_id_fk" FOREIGN KEY ("question_id") REFERENCES "public"."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_question_id_questions_id_fk" FOREIGN KEY ("question_id") REFERENCES "public"."questions"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/migrations/meta/0027_snapshot.json b/migrations/meta/0027_snapshot.json new file mode 100644 index 00000000..d8f89b05 --- /dev/null +++ b/migrations/meta/0027_snapshot.json @@ -0,0 +1,1647 @@ +{ + "id": "201665be-855d-4af0-8e69-731c400b4907", + "prevId": "3e25dac0-f63a-429e-beec-e0b014693359", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.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": {} + }, + "public.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 + }, + "option_id": { + "name": "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_option_id_options_id_fk": { + "name": "comments_option_id_options_id_fk", + "tableFrom": "comments", + "tableTo": "options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.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 + }, + "require_approval": { + "name": "require_approval", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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": {} + }, + "public.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" + ] + } + } + }, + "public.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 + }, + "required": { + "name": "required", + "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": {} + }, + "public.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": 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": { + "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" + ] + } + } + }, + "public.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" + ] + } + } + }, + "public.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": {} + }, + "public.questions": { + "name": "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 + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "sub_title": { + "name": "sub_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "vote_model": { + "name": "vote_model", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "default": "'COCM'" + }, + "show_score": { + "name": "show_score", + "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": { + "questions_cycle_id_cycles_id_fk": { + "name": "questions_cycle_id_cycles_id_fk", + "tableFrom": "questions", + "tableTo": "cycles", + "columnsFrom": [ + "cycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.options": { + "name": "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 + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "sub_title": { + "name": "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'" + }, + "funding_request": { + "name": "funding_request", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "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": { + "options_user_id_users_id_fk": { + "name": "options_user_id_users_id_fk", + "tableFrom": "options", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_registration_id_registrations_id_fk": { + "name": "options_registration_id_registrations_id_fk", + "tableFrom": "options", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_question_id_questions_id_fk": { + "name": "options_question_id_questions_id_fk", + "tableFrom": "options", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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_options_id_fk": { + "name": "votes_option_id_options_id_fk", + "tableFrom": "votes", + "tableTo": "options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_question_id_questions_id_fk": { + "name": "votes_question_id_questions_id_fk", + "tableFrom": "votes", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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 + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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" + ] + } + } + }, + "public.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": {} + }, + "public.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_questions_id_fk": { + "name": "questions_to_group_categories_question_id_questions_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "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 06671681..6cb5fc89 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -190,6 +190,13 @@ "when": 1719999199277, "tag": "0026_redundant_galactus", "breakpoints": true + }, + { + "idx": 27, + "version": "6", + "when": 1720035204142, + "tag": "0027_stormy_silverclaw", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/comments.ts b/src/db/comments.ts index e378c0d0..550184a4 100644 --- a/src/db/comments.ts +++ b/src/db/comments.ts @@ -1,13 +1,13 @@ import { relations } from 'drizzle-orm'; import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; -import { questionOptions } from './question-options'; +import { options } from './options'; import { users } from './users'; import { likes } from './likes'; export const comments = pgTable('comments', { id: uuid('id').primaryKey().defaultRandom(), userId: uuid('user_id').references(() => users.id), - questionOptionId: uuid('question_option_id').references(() => questionOptions.id), + optionId: uuid('option_id').references(() => options.id), value: varchar('value').notNull(), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), @@ -18,9 +18,9 @@ export const commentsRelations = relations(comments, ({ one, many }) => ({ fields: [comments.userId], references: [users.id], }), - questionOption: one(questionOptions, { - fields: [comments.questionOptionId], - references: [questionOptions.id], + option: one(options, { + fields: [comments.optionId], + references: [options.id], }), likes: many(likes), })); diff --git a/src/db/cycles.ts b/src/db/cycles.ts index 4d0549d4..834c0e01 100644 --- a/src/db/cycles.ts +++ b/src/db/cycles.ts @@ -1,6 +1,6 @@ import { relations } from 'drizzle-orm'; import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; -import { forumQuestions } from './forum-questions'; +import { questions } from './questions'; import { events } from './events'; export const cycles = pgTable('cycles', { @@ -17,7 +17,7 @@ export const cycles = pgTable('cycles', { }); export const cyclesRelations = relations(cycles, ({ many, one }) => ({ - forumQuestions: many(forumQuestions), + questions: many(questions), event: one(events, { fields: [cycles.eventId], references: [events.id], diff --git a/src/db/index.ts b/src/db/index.ts index eabeebc9..b8c5be0e 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -2,9 +2,9 @@ export * from './users'; export * from './federated-credentials'; export * from './registrations'; export * from './cycles'; -export * from './forum-questions'; +export * from './questions'; export * from './registration-field-options'; -export * from './question-options'; +export * from './options'; export * from './votes'; export * from './events'; export * from './registration-fields'; diff --git a/src/db/question-options.ts b/src/db/options.ts similarity index 61% rename from src/db/question-options.ts rename to src/db/options.ts index a9bd601d..4fd771e3 100644 --- a/src/db/question-options.ts +++ b/src/db/options.ts @@ -1,20 +1,20 @@ import { boolean, pgTable, timestamp, uuid, varchar, numeric } from 'drizzle-orm/pg-core'; -import { forumQuestions } from './forum-questions'; +import { questions } from './questions'; import { relations } from 'drizzle-orm'; import { votes } from './votes'; import { registrations } from './registrations'; import { comments } from './comments'; import { users } from './users'; -export const questionOptions = pgTable('question_options', { +export const options = pgTable('options', { id: uuid('id').primaryKey().defaultRandom(), userId: uuid('user_id').references(() => users.id), registrationId: uuid('registration_id').references(() => registrations.id), questionId: uuid('question_id') - .references(() => forumQuestions.id) + .references(() => questions.id) .notNull(), - optionTitle: varchar('option_title', { length: 256 }).notNull(), - optionSubTitle: varchar('option_sub_title'), + title: varchar('title', { length: 256 }).notNull(), + subTitle: varchar('sub_title'), accepted: boolean('accepted').default(false), voteScore: numeric('vote_score').notNull().default('0.0'), fundingRequest: numeric('funding_request').default('0.0'), @@ -22,21 +22,21 @@ export const questionOptions = pgTable('question_options', { updatedAt: timestamp('updated_at').notNull().defaultNow(), }); -export const questionOptionsRelations = relations(questionOptions, ({ one, many }) => ({ +export const questionOptionsRelations = relations(options, ({ one, many }) => ({ user: one(users, { - fields: [questionOptions.userId], + fields: [options.userId], references: [users.id], }), - forumQuestion: one(forumQuestions, { - fields: [questionOptions.questionId], - references: [forumQuestions.id], + question: one(questions, { + fields: [options.questionId], + references: [questions.id], }), registrations: one(registrations, { - fields: [questionOptions.registrationId], + fields: [options.registrationId], references: [registrations.id], }), comment: many(comments), votes: many(votes), })); -export type QuestionOption = typeof questionOptions.$inferSelect; +export type Option = typeof options.$inferSelect; diff --git a/src/db/questions-to-group-categories.ts b/src/db/questions-to-group-categories.ts index ef1af337..8ec5a880 100644 --- a/src/db/questions-to-group-categories.ts +++ b/src/db/questions-to-group-categories.ts @@ -1,13 +1,13 @@ import { pgTable, timestamp, uuid } from 'drizzle-orm/pg-core'; import { relations } from 'drizzle-orm'; import { groupCategories } from './group-categories'; -import { forumQuestions } from './forum-questions'; +import { questions } from './questions'; export const questionsToGroupCategories = pgTable('questions_to_group_categories', { id: uuid('id').primaryKey().defaultRandom(), questionId: uuid('question_id') .notNull() - .references(() => forumQuestions.id), + .references(() => questions.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(), @@ -16,9 +16,9 @@ export const questionsToGroupCategories = pgTable('questions_to_group_categories export const questionsToGroupCategoriesRelations = relations( questionsToGroupCategories, ({ one }) => ({ - question: one(forumQuestions, { + question: one(questions, { fields: [questionsToGroupCategories.questionId], - references: [forumQuestions.id], + references: [questions.id], }), groupCategory: one(groupCategories, { fields: [questionsToGroupCategories.groupCategoryId], diff --git a/src/db/forum-questions.ts b/src/db/questions.ts similarity index 60% rename from src/db/forum-questions.ts rename to src/db/questions.ts index 031d1200..97530241 100644 --- a/src/db/forum-questions.ts +++ b/src/db/questions.ts @@ -1,29 +1,29 @@ import { boolean, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import { cycles } from './cycles'; import { relations } from 'drizzle-orm'; -import { questionOptions } from './question-options'; +import { options } from './options'; import { questionsToGroupCategories } from './questions-to-group-categories'; -export const forumQuestions = pgTable('forum_questions', { +export const questions = pgTable('questions', { id: uuid('id').primaryKey().defaultRandom(), cycleId: uuid('cycle_id') .references(() => cycles.id) .notNull(), - questionTitle: varchar('question_title', { length: 256 }).notNull(), - questionSubTitle: varchar('question_sub_title', { length: 256 }), + title: varchar('title', { length: 256 }).notNull(), + subTitle: varchar('sub_title', { length: 256 }), voteModel: varchar('vote_model', { length: 256 }).notNull().default('COCM'), showScore: boolean('show_score').default(false), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); -export const forumQuestionsRelations = relations(forumQuestions, ({ one, many }) => ({ +export const forumQuestionsRelations = relations(questions, ({ one, many }) => ({ cycle: one(cycles, { - fields: [forumQuestions.cycleId], + fields: [questions.cycleId], references: [cycles.id], }), - questionOptions: many(questionOptions), + options: many(options), questionsToGroupCategories: many(questionsToGroupCategories), })); -export type ForumQuestion = typeof forumQuestions.$inferSelect; +export type Question = typeof questions.$inferSelect; diff --git a/src/db/users.ts b/src/db/users.ts index 07e19ff2..9317c47c 100644 --- a/src/db/users.ts +++ b/src/db/users.ts @@ -3,7 +3,7 @@ import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import { federatedCredentials } from './federated-credentials'; import { comments } from './comments'; import { likes } from './likes'; -import { questionOptions } from './question-options'; +import { options } from './options'; import { registrations } from './registrations'; import { userAttributes } from './user-attributes'; import { usersToGroups } from './users-to-groups'; @@ -29,7 +29,7 @@ export const usersRelations = relations(users, ({ many, one }) => ({ federatedCredential: one(federatedCredentials), comments: many(comments), likes: many(likes), - questionOptions: many(questionOptions), + options: many(options), notifications: many(usersToNotifications), })); diff --git a/src/db/votes.ts b/src/db/votes.ts index ebedb537..fd77f846 100644 --- a/src/db/votes.ts +++ b/src/db/votes.ts @@ -1,8 +1,8 @@ import { integer, pgTable, timestamp, uuid } from 'drizzle-orm/pg-core'; import { users } from './users'; import { relations } from 'drizzle-orm'; -import { questionOptions } from './question-options'; -import { forumQuestions } from './forum-questions'; +import { options } from './options'; +import { questions } from './questions'; export const votes = pgTable('votes', { id: uuid('id').primaryKey().defaultRandom(), @@ -11,10 +11,10 @@ export const votes = pgTable('votes', { .references(() => users.id), optionId: uuid('option_id') .notNull() - .references(() => questionOptions.id), + .references(() => options.id), questionId: uuid('question_id') .notNull() - .references(() => forumQuestions.id), + .references(() => questions.id), numOfVotes: integer('num_of_votes').notNull(), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), @@ -25,13 +25,13 @@ export const votesRelations = relations(votes, ({ one }) => ({ fields: [votes.userId], references: [users.id], }), - questionOptions: one(questionOptions, { + option: one(options, { fields: [votes.optionId], - references: [questionOptions.id], + references: [options.id], }), - forumQuestion: one(forumQuestions, { + question: one(questions, { fields: [votes.questionId], - references: [forumQuestions.id], + references: [questions.id], }), })); diff --git a/src/handlers/comments.ts b/src/handlers/comments.ts index 66005625..ea5e0a9e 100644 --- a/src/handlers/comments.ts +++ b/src/handlers/comments.ts @@ -85,7 +85,7 @@ export function saveCommentHandler(dbPool: NodePgDatabase) { return res.status(400).json({ errors: body.error.issues }); } - const canComment = await userCanComment(dbPool, userId, body.data.questionOptionId); + const canComment = await userCanComment(dbPool, userId, body.data.optionId); if (!canComment) { return res.status(403).json({ errors: [{ message: 'User cannot comment on this option' }] }); diff --git a/src/handlers/cycles.ts b/src/handlers/cycles.ts index 911d9932..4e383f33 100644 --- a/src/handlers/cycles.ts +++ b/src/handlers/cycles.ts @@ -9,13 +9,13 @@ export function getActiveCyclesHandler(dbPool: NodePgDatabase) { const activeCycles = await dbPool.query.cycles.findMany({ where: and(lte(db.cycles.startAt, new Date()), gte(db.cycles.endAt, new Date())), with: { - forumQuestions: { + questions: { with: { - questionOptions: { + options: { columns: { voteScore: false, }, - where: eq(db.questionOptions.accepted, true), + where: eq(db.options.accepted, true), }, }, }, diff --git a/src/handlers/events.ts b/src/handlers/events.ts index d2058b25..743b9069 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -14,13 +14,13 @@ export function getEventCyclesHandler(dbPool: NodePgDatabase) { const eventCycles = await dbPool.query.cycles.findMany({ where: eq(db.cycles.eventId, eventId), with: { - forumQuestions: { + questions: { with: { - questionOptions: { + options: { columns: { voteScore: false, }, - where: eq(db.questionOptions.accepted, true), + where: eq(db.options.accepted, true), }, }, }, diff --git a/src/handlers/options.ts b/src/handlers/options.ts index ee065ce1..b4faadf8 100644 --- a/src/handlers/options.ts +++ b/src/handlers/options.ts @@ -12,14 +12,14 @@ export function getOptionHandler(dbPool: NodePgDatabase) { return res.status(400).json({ error: 'Missing optionId' }); } - const { voteScore, ...rest } = getTableColumns(db.questionOptions); + const { voteScore, ...rest } = getTableColumns(db.options); const rows = await dbPool .select({ ...rest, }) - .from(db.questionOptions) - .where(eq(db.questionOptions.id, optionId)); + .from(db.options) + .where(eq(db.options.id, optionId)); if (!rows.length) { return res.status(404).json({ error: 'Option not found' }); diff --git a/src/handlers/forum-questions.ts b/src/handlers/questions.ts similarity index 100% rename from src/handlers/forum-questions.ts rename to src/handlers/questions.ts diff --git a/src/handlers/users.ts b/src/handlers/users.ts index 1cc33fde..fe607a39 100644 --- a/src/handlers/users.ts +++ b/src/handlers/users.ts @@ -164,11 +164,11 @@ export function getUserOptionsHandler(dbPool: NodePgDatabase) { return res.status(400).json({ error: 'Missing userId' }); } - const optionsQuery = await dbPool.query.questionOptions.findMany({ + const optionsQuery = await dbPool.query.options.findMany({ with: { - forumQuestion: true, + question: true, }, - where: eq(db.questionOptions.userId, userId), + where: eq(db.options.userId, userId), }); return res.json({ data: optionsQuery }); diff --git a/src/routers/api.ts b/src/routers/api.ts index e30e815b..29ea428a 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -7,7 +7,7 @@ import cors from 'cors'; import { usersRouter } from './users'; import { cyclesRouter } from './cycles'; import { eventsRouter } from './events'; -import { forumQuestionsRouter } from './forum-questions'; +import { forumQuestionsRouter } from './questions'; import { groupsRouter } from './groups'; import { commentsRouter } from './comments'; import { optionsRouter } from './options'; @@ -54,7 +54,7 @@ export function apiRouter({ router.use('/cycles', cyclesRouter({ dbPool })); router.use('/votes', votesRouter({ dbPool })); router.use('/events', eventsRouter({ dbPool })); - router.use('/forum-questions', forumQuestionsRouter({ dbPool })); + router.use('/questions', forumQuestionsRouter({ dbPool })); router.use('/groups', groupsRouter({ dbPool })); router.use('/comments', commentsRouter({ dbPool })); router.use('/options', optionsRouter({ dbPool })); diff --git a/src/routers/forum-questions.ts b/src/routers/questions.ts similarity index 95% rename from src/routers/forum-questions.ts rename to src/routers/questions.ts index e515a588..d52d5f46 100644 --- a/src/routers/forum-questions.ts +++ b/src/routers/questions.ts @@ -5,7 +5,7 @@ import { getCalculateFundingHandler, getQuestionHeartsHandler, getResultStatisticsHandler, -} from '../handlers/forum-questions'; +} from '../handlers/questions'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); diff --git a/src/services/comments.spec.ts b/src/services/comments.spec.ts index 414fa51e..5396aeb6 100644 --- a/src/services/comments.spec.ts +++ b/src/services/comments.spec.ts @@ -14,7 +14,7 @@ describe('service: comments', () => { let dbConnection: Client; let groupRegistrationData: z.infer; let secretCategory: db.GroupCategory | undefined; - let questionOption: db.QuestionOption | undefined; + let questionOption: db.Option | undefined; let secretGroup: db.Group[]; let cycle: db.Cycle | undefined; let user: db.User | undefined; @@ -71,9 +71,9 @@ describe('service: comments', () => { // update question options await dbPool - .update(db.questionOptions) + .update(db.options) .set({ registrationId: registrationId!, userId: user?.id ?? '' }) - .where(eq(db.questionOptions.id, questionOption!.id)); + .where(eq(db.options.id, questionOption!.id)); // update secret group await dbPool.update(db.groups).set({ secret: '12345' }).where(eq(db.groups.id, secretGroupId)); diff --git a/src/services/comments.ts b/src/services/comments.ts index 371368d9..10813f72 100644 --- a/src/services/comments.ts +++ b/src/services/comments.ts @@ -22,7 +22,7 @@ export async function saveComment( .insert(db.comments) .values({ userId: userId, - questionOptionId: data.questionOptionId, + optionId: data.optionId, value: data.value, }) .returning(); @@ -80,13 +80,13 @@ export async function getOptionComments( .select() .from(db.comments) .leftJoin(db.users, eq(db.comments.userId, db.users.id)) - .where(eq(db.comments.questionOptionId, data.optionId)); + .where(eq(db.comments.optionId, data.optionId)); const commentsWithUserNames = rows.map((row) => { return { id: row.comments.id, userId: row.comments.userId, - questionOptionId: row.comments.questionOptionId, + optionId: row.comments.optionId, value: row.comments.value, createdAt: row.comments.createdAt, user: { @@ -205,15 +205,15 @@ export async function getOptionUsers( option_owner AS ( SELECT - question_options."id", users."id" AS "user_id", + options."id", users."id" AS "user_id", json_build_object( 'id', users."id", 'username', users."username", 'firstName', users."first_name", 'lastName', users."last_name" ) AS option_owner - FROM question_options - LEFT JOIN users ON question_options."user_id" = users."id" + FROM options + LEFT JOIN users ON options."user_id" = users."id" ), registrations_secret_groups AS ( @@ -225,16 +225,16 @@ export async function getOptionUsers( result AS ( SELECT - question_options."id" AS "optionId", - question_options."registration_id" AS "registrationId", - question_options."user_id" AS "userId", + options."id" AS "optionId", + options."registration_id" AS "registrationId", + options."user_id" AS "userId", option_owner."option_owner" AS "user", registrations_secret_groups."group_id" AS "groupId", registrations_secret_groups."users_in_group" AS "usersInGroup" - FROM question_options - LEFT JOIN registrations_secret_groups ON question_options."registration_id" = registrations_secret_groups."id" - LEFT JOIN option_owner ON question_options."user_id" = option_owner."user_id" - WHERE question_options."id" = '${optionId}' + FROM options + LEFT JOIN registrations_secret_groups ON options."registration_id" = registrations_secret_groups."id" + LEFT JOIN option_owner ON options."user_id" = option_owner."user_id" + WHERE options."id" = '${optionId}' ), nested_result AS ( diff --git a/src/services/cycles.spec.ts b/src/services/cycles.spec.ts index e5ad1bc8..1575a44d 100644 --- a/src/services/cycles.spec.ts +++ b/src/services/cycles.spec.ts @@ -11,8 +11,8 @@ describe('service: cycles', () => { let dbPool: NodePgDatabase; let dbConnection: Client; let cycle: db.Cycle | undefined; - let questionOption: db.QuestionOption | undefined; - let forumQuestion: db.ForumQuestion | undefined; + let questionOption: db.Option | undefined; + let forumQuestion: db.Question | undefined; let user: db.User | undefined; let secondUser: db.User | undefined; diff --git a/src/services/cycles.ts b/src/services/cycles.ts index 26e0f93d..5825fac4 100644 --- a/src/services/cycles.ts +++ b/src/services/cycles.ts @@ -6,9 +6,9 @@ export async function GetCycleById(dbPool: NodePgDatabase, cycleId: s const cycle = await dbPool.query.cycles.findFirst({ where: eq(db.cycles.id, cycleId), with: { - forumQuestions: { + questions: { with: { - questionOptions: { + options: { with: { user: { with: { @@ -27,7 +27,7 @@ export async function GetCycleById(dbPool: NodePgDatabase, cycleId: s }, }, }, - where: eq(db.questionOptions.accepted, true), + where: eq(db.options.accepted, true), }, }, }, @@ -36,15 +36,15 @@ export async function GetCycleById(dbPool: NodePgDatabase, cycleId: s const out = { ...cycle, - forumQuestions: cycle?.forumQuestions.map((question) => { + forumQuestions: cycle?.questions.map((question) => { return { ...question, - questionOptions: question.questionOptions.map((option) => { + questionOptions: question.options.map((option) => { return { id: option.id, accepted: option.accepted, - optionTitle: option.optionTitle, - optionSubTitle: option.optionSubTitle, + title: option.title, + subTitle: option.subTitle, questionId: option.questionId, voteScore: question.showScore ? option.voteScore : undefined, registrationId: option.registrationId, @@ -79,9 +79,9 @@ export async function getCycleVotes( ) { const response = await dbPool.query.cycles.findMany({ with: { - forumQuestions: { + questions: { with: { - questionOptions: { + options: { columns: { voteScore: false, }, @@ -104,9 +104,7 @@ export async function getCycleVotes( }); const out = response.flatMap((cycle) => - cycle.forumQuestions.flatMap((question) => - question.questionOptions.flatMap((option) => option.votes), - ), + cycle.questions.flatMap((question) => question.options.flatMap((option) => option.votes)), ); return out; diff --git a/src/services/funding-mechanism.ts b/src/services/funding-mechanism.ts index 85e52d0a..405801b0 100644 --- a/src/services/funding-mechanism.ts +++ b/src/services/funding-mechanism.ts @@ -17,12 +17,12 @@ export async function calculateFunding( ): Promise<{ allocated_funding: { [key: string]: number }; remaining_funding: number }> { const getOptionData = await dbPool .select({ - id: db.questionOptions.id, - voteScore: db.questionOptions.voteScore, - fundingRequest: db.questionOptions.fundingRequest, + id: db.options.id, + voteScore: db.options.voteScore, + fundingRequest: db.options.fundingRequest, }) - .from(db.questionOptions) - .where(eq(db.questionOptions.questionId, forumQuestionId)); + .from(db.options) + .where(eq(db.options.questionId, forumQuestionId)); if (!getOptionData) { throw new Error('Error in query getOptionData'); diff --git a/src/services/statistics.spec.ts b/src/services/statistics.spec.ts index a4c3c2e6..57eefa62 100644 --- a/src/services/statistics.spec.ts +++ b/src/services/statistics.spec.ts @@ -13,8 +13,8 @@ describe('service: statistics', () => { let dbConnection: Client; let userTestData: z.infer; let otherUserTestData: z.infer; - let questionOption: db.QuestionOption | undefined; - let forumQuestion: db.ForumQuestion | undefined; + let questionOption: db.Option | undefined; + let forumQuestion: db.Question | undefined; let user: db.User | undefined; let otherUser: db.User | undefined; @@ -83,8 +83,8 @@ describe('service: statistics', () => { for (const optionId in result.optionStats) { const optionStat = result.optionStats[optionId]; expect(optionStat).toBeDefined(); - expect(optionStat?.optionTitle).toBeDefined(); - expect(optionStat?.optionSubTitle).toBeDefined(); + expect(optionStat?.title).toBeDefined(); + expect(optionStat?.subTitle).toBeDefined(); expect(optionStat?.pluralityScore).toBeDefined(); expect(optionStat?.distinctUsers).toBeDefined(); expect(optionStat?.allocatedHearts).toBeDefined(); diff --git a/src/services/statistics.ts b/src/services/statistics.ts index 86bc1486..1fc476fd 100644 --- a/src/services/statistics.ts +++ b/src/services/statistics.ts @@ -10,8 +10,8 @@ type ResultData = { optionStats: Record< string, { - optionTitle: string; - optionSubTitle: string; + title: string; + subTitle: string; pluralityScore: string; distinctUsers: number; allocatedHearts: number; @@ -46,7 +46,7 @@ export async function executeResultQueries( dbPool.execute<{ numProposals: number }>( sql.raw(` SELECT count("id")::int AS "numProposals" - FROM question_options + FROM options WHERE question_id = '${forumQuestionId}' AND accepted = TRUE `), @@ -100,8 +100,8 @@ export async function executeResultQueries( // Get individual results dbPool.execute<{ optionId: string; - optionTitle: string; - optionSubTitle: string; + title: string; + subTitle: string; pluralityScore: string; distinctUsers: number; allocatedHearts: number; @@ -118,8 +118,8 @@ export async function executeResultQueries( ), 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 + SELECT "id" AS "optionId", "title" AS "title", "sub_title" AS "subTitle", vote_score AS "pluralityScore" + FROM options WHERE question_id = '${forumQuestionId}' AND accepted = TRUE -- makes sure to only expose data of accepted options ), @@ -189,8 +189,8 @@ export async function executeResultQueries( /* Aggregated results */ merged_result AS ( SELECT id_title_score."optionId", - id_title_score."optionTitle", - id_title_score."optionSubTitle", + id_title_score."title", + id_title_score."subTitle", id_title_score."pluralityScore", distinct_users."distinctUsers", hearts."allocatedHearts", @@ -221,8 +221,8 @@ export async function executeResultQueries( const indivStats: Record< string, { - optionTitle: string; - optionSubTitle: string; + title: string; + subTitle: string; pluralityScore: string; distinctUsers: number; allocatedHearts: number; @@ -236,8 +236,8 @@ export async function executeResultQueries( queryIndivStatistics.rows.forEach((row) => { const { optionId: indivOptionId, - optionTitle: indivOptionTitle, - optionSubTitle: indivOptionSubTitle, + title: indivOptionTitle, + subTitle: indivOptionSubTitle, pluralityScore: indivPluralityScore, distinctUsers: indivDistinctUsers, allocatedHearts: indivAllocatedHearts, @@ -247,8 +247,8 @@ export async function executeResultQueries( } = row; indivStats[indivOptionId] = { - optionTitle: indivOptionTitle || 'No Title Provided', - optionSubTitle: indivOptionSubTitle || '', + title: indivOptionTitle || 'No Title Provided', + subTitle: indivOptionSubTitle || '', pluralityScore: indivPluralityScore || '0.0', distinctUsers: indivDistinctUsers || 0, allocatedHearts: indivAllocatedHearts || 0, diff --git a/src/services/votes.spec.ts b/src/services/votes.spec.ts index d2b6b426..5f7e75ba 100644 --- a/src/services/votes.spec.ts +++ b/src/services/votes.spec.ts @@ -25,10 +25,10 @@ describe('service: votes', () => { let dbConnection: Client; let testData: z.infer; let cycle: db.Cycle | undefined; - let questionOption: db.QuestionOption | undefined; - let otherQuestionOption: db.QuestionOption | undefined; - let forumQuestion: db.ForumQuestion | undefined; - let otherForumQuestion: db.ForumQuestion | undefined; + let questionOption: db.Option | undefined; + let otherQuestionOption: db.Option | undefined; + let forumQuestion: db.Question | undefined; + let otherForumQuestion: db.Question | undefined; let groupCategory: db.GroupCategory | undefined; let otherGroupCategory: db.GroupCategory | undefined; let unrelatedGroupCategory: db.GroupCategory | undefined; @@ -325,8 +325,8 @@ describe('service: votes', () => { await updateVoteScoreInDatabase(dbPool, questionOption?.id ?? '', score); // query updated score in db - const updatedDbScore = await dbPool.query.questionOptions.findFirst({ - where: eq(db.questionOptions.id, questionOption?.id ?? ''), + const updatedDbScore = await dbPool.query.options.findFirst({ + where: eq(db.options.id, questionOption?.id ?? ''), }); expect(updatedDbScore?.voteScore).toBe('100'); diff --git a/src/services/votes.ts b/src/services/votes.ts index 416f7d91..e6e6bed1 100644 --- a/src/services/votes.ts +++ b/src/services/votes.ts @@ -37,8 +37,8 @@ export async function saveVotes( } } - const queryQuestionOption = await dbPool.query.questionOptions.findFirst({ - where: eq(db.questionOptions.id, voteData[0]!.optionId), + const queryQuestionOption = await dbPool.query.options.findFirst({ + where: eq(db.options.id, voteData[0]!.optionId), }); if (!queryQuestionOption) { @@ -46,8 +46,8 @@ export async function saveVotes( return { data: voteData, errors }; } - const queryForumQuestion = await dbPool.query.forumQuestions.findFirst({ - where: eq(db.forumQuestions.id, queryQuestionOption!.questionId), + const queryForumQuestion = await dbPool.query.questions.findFirst({ + where: eq(db.questions.id, queryQuestionOption!.questionId), }); if (!queryForumQuestion) { @@ -207,12 +207,12 @@ export async function updateVoteScoreInDatabase( ) { // Update vote score in the database await dbPool - .update(db.questionOptions) + .update(db.options) .set({ voteScore: score.toString(), updatedAt: new Date(), }) - .where(eq(db.questionOptions.id, optionId)); + .where(eq(db.options.id, optionId)); } /** @@ -236,10 +236,10 @@ export async function updateVoteScorePlural( // Query group data, grouping dimensions, and calculate the score const queryQuestionId = await dbPool .select({ - questionId: db.questionOptions.questionId, + questionId: db.options.questionId, }) - .from(db.questionOptions) - .where(eq(db.questionOptions.id, optionId)); + .from(db.options) + .where(eq(db.options.id, optionId)); const groupCategories = await queryGroupCategories(dbPool, queryQuestionId[0]!.questionId); const groupArray = await groupsDictionary(dbPool, votesDictionary, groupCategories ?? []); @@ -292,8 +292,8 @@ async function validateAndSaveVote( return { data: null, error: 'optionId is required' }; } - const queryQuestionOption = await dbPool.query.questionOptions.findFirst({ - where: eq(db.questionOptions.id, vote.optionId), + const queryQuestionOption = await dbPool.query.options.findFirst({ + where: eq(db.options.id, vote.optionId), }); if (!queryQuestionOption) { @@ -346,8 +346,8 @@ export async function saveVote( vote: z.infer, ) { // check if cycle is open - const queryQuestion = await dbPool.query.forumQuestions.findFirst({ - where: eq(db.forumQuestions.id, vote?.questionId ?? ''), + const queryQuestion = await dbPool.query.questions.findFirst({ + where: eq(db.questions.id, vote?.questionId ?? ''), with: { cycle: true, }, diff --git a/src/utils/db/seed-data-generators.ts b/src/utils/db/seed-data-generators.ts index db90ae30..70b489f6 100644 --- a/src/utils/db/seed-data-generators.ts +++ b/src/utils/db/seed-data-generators.ts @@ -2,10 +2,10 @@ import { randCompanyName, randCountry, randUser } from '@ngneat/falso'; import { Cycle, Event, - ForumQuestion, + Question, RegistrationField, RegistrationFieldOption, - QuestionOption, + Option, GroupCategory, Group, User, @@ -24,8 +24,8 @@ export type RegistrationFieldOptionData = Pick< RegistrationFieldOption, 'registrationFieldId' | 'value' >; -export type ForumQuestionData = Pick; -export type QuestionOptionData = Pick; +export type ForumQuestionData = Pick; +export type QuestionOptionData = Pick; export type GroupCategoryData = Pick< GroupCategory, 'name' | 'eventId' | 'userCanCreate' | 'userCanView' | 'required' @@ -95,9 +95,9 @@ export function generateForumQuestionData( questionTitles: string[], voteModels: string[], ): ForumQuestionData[] { - return questionTitles.map((questionTitle, index) => ({ + return questionTitles.map((title, index) => ({ cycleId, - questionTitle, + title, voteModel: voteModels[index] ?? 'COCM', })); } @@ -112,7 +112,7 @@ export function generateQuestionOptionsData( for (let i = 0; i < optionTitles.length; i++) { const optionData: QuestionOptionData = { questionId, - optionTitle: optionTitles[i]!, + title: optionTitles[i]!, accepted: status[i]!, }; questionOptionsData.push(optionData); diff --git a/src/utils/db/seed.ts b/src/utils/db/seed.ts index 3614998c..bf01456a 100644 --- a/src/utils/db/seed.ts +++ b/src/utils/db/seed.ts @@ -130,7 +130,7 @@ async function cleanup(dbPool: NodePgDatabase) { await dbPool.delete(db.userAttributes); await dbPool.delete(db.votes); await dbPool.delete(db.federatedCredentials); - await dbPool.delete(db.questionOptions); + await dbPool.delete(db.options); await dbPool.delete(db.registrationData); await dbPool.delete(db.registrationFieldOptions); await dbPool.delete(db.registrationFields); @@ -140,7 +140,7 @@ async function cleanup(dbPool: NodePgDatabase) { await dbPool.delete(db.groups); await dbPool.delete(db.questionsToGroupCategories); await dbPool.delete(db.groupCategories); - await dbPool.delete(db.forumQuestions); + await dbPool.delete(db.questions); await dbPool.delete(db.cycles); await dbPool.delete(db.events); } @@ -261,10 +261,10 @@ async function createForumQuestions( } const result = await dbPool - .insert(db.forumQuestions) + .insert(db.questions) .values({ cycleId: questionData.cycleId, - questionTitle: questionData.questionTitle, + title: questionData.title, voteModel: questionData.voteModel, }) .returning(); @@ -290,10 +290,10 @@ async function createQuestionOptions( } const result = await dbPool - .insert(db.questionOptions) + .insert(db.options) .values({ questionId: questionOption.questionId, - optionTitle: questionOption.optionTitle, + title: questionOption.title, accepted: questionOption.accepted, }) .returning(); From 4235d5c5c2678d58a22eda7890a29b369775aa2a Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Tue, 9 Jul 2024 10:32:56 +0100 Subject: [PATCH 03/14] Migrate to jsonb (#442) * initial event registration changes * get validation working on event registrations * fix handler validation * add new fields to options and questions * add linking group to option * fix tests * minor naming fixes * rename validateEventRegistrationFields to validateEventFields, to keep consistency with options * seed event fields * add fields to seed function * make sure fields seed with uuid * check if event exists --------- Co-authored-by: Martin Benedikt Busch --- migrations/0028_green_gamora.sql | 5 + migrations/0029_colorful_psynapse.sql | 6 + migrations/meta/0028_snapshot.json | 1673 +++++++++++++++++++++ migrations/meta/0029_snapshot.json | 1692 ++++++++++++++++++++++ migrations/meta/_journal.json | 14 + src/db/events.ts | 6 +- src/db/groups.ts | 2 + src/db/options.ts | 11 +- src/db/questions.ts | 3 +- src/db/registrations.ts | 3 +- src/handlers/cycles.ts | 2 +- src/handlers/events.ts | 2 +- src/handlers/registrations.ts | 30 +- src/services/cycles.ts | 4 +- src/services/forum-questions.ts | 41 +- src/services/registration-data.spec.ts | 130 -- src/services/registration-data.ts | 71 - src/services/registration-fields.spec.ts | 145 -- src/services/registration-fields.ts | 64 - src/services/registrations.spec.ts | 168 ++- src/services/registrations.ts | 78 +- src/services/statistics.ts | 4 +- src/services/validation.spec.ts | 382 +++++ src/services/validation.ts | 116 ++ src/types/index.ts | 2 + src/types/options.ts | 7 + src/types/registrations.ts | 14 +- src/types/validation.ts | 45 + src/utils/db/seed-data-generators.ts | 34 +- src/utils/db/seed.ts | 7 +- 30 files changed, 4204 insertions(+), 557 deletions(-) create mode 100644 migrations/0028_green_gamora.sql create mode 100644 migrations/0029_colorful_psynapse.sql create mode 100644 migrations/meta/0028_snapshot.json create mode 100644 migrations/meta/0029_snapshot.json delete mode 100644 src/services/registration-data.spec.ts delete mode 100644 src/services/registration-data.ts delete mode 100644 src/services/registration-fields.spec.ts delete mode 100644 src/services/registration-fields.ts create mode 100644 src/services/validation.spec.ts create mode 100644 src/services/validation.ts create mode 100644 src/types/options.ts create mode 100644 src/types/validation.ts diff --git a/migrations/0028_green_gamora.sql b/migrations/0028_green_gamora.sql new file mode 100644 index 00000000..00a23f02 --- /dev/null +++ b/migrations/0028_green_gamora.sql @@ -0,0 +1,5 @@ +ALTER TABLE "options" RENAME COLUMN "accepted" TO "show";--> statement-breakpoint +ALTER TABLE "events" ADD COLUMN "fields" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint +ALTER TABLE "registrations" ADD COLUMN "data" jsonb;--> statement-breakpoint +ALTER TABLE "questions" ADD COLUMN "fields" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint +ALTER TABLE "options" ADD COLUMN "data" jsonb; \ No newline at end of file diff --git a/migrations/0029_colorful_psynapse.sql b/migrations/0029_colorful_psynapse.sql new file mode 100644 index 00000000..c5d35b7d --- /dev/null +++ b/migrations/0029_colorful_psynapse.sql @@ -0,0 +1,6 @@ +ALTER TABLE "options" ADD COLUMN "group_id" uuid;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "options" ADD CONSTRAINT "options_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/migrations/meta/0028_snapshot.json b/migrations/meta/0028_snapshot.json new file mode 100644 index 00000000..a199029b --- /dev/null +++ b/migrations/meta/0028_snapshot.json @@ -0,0 +1,1673 @@ +{ + "id": "3b3f3fe7-aab6-4ea0-88cd-e1c81a678986", + "prevId": "201665be-855d-4af0-8e69-731c400b4907", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.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": {} + }, + "public.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 + }, + "option_id": { + "name": "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_option_id_options_id_fk": { + "name": "comments_option_id_options_id_fk", + "tableFrom": "comments", + "tableTo": "options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.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 + }, + "require_approval": { + "name": "require_approval", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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 + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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": {} + }, + "public.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" + ] + } + } + }, + "public.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 + }, + "required": { + "name": "required", + "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": {} + }, + "public.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": 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": { + "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" + ] + } + } + }, + "public.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" + ] + } + } + }, + "public.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'" + }, + "data": { + "name": "data", + "type": "jsonb", + "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": { + "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": {} + }, + "public.questions": { + "name": "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 + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "sub_title": { + "name": "sub_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "vote_model": { + "name": "vote_model", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "default": "'COCM'" + }, + "show_score": { + "name": "show_score", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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_cycle_id_cycles_id_fk": { + "name": "questions_cycle_id_cycles_id_fk", + "tableFrom": "questions", + "tableTo": "cycles", + "columnsFrom": [ + "cycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.options": { + "name": "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 + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "sub_title": { + "name": "sub_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "show": { + "name": "show", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vote_score": { + "name": "vote_score", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0.0'" + }, + "funding_request": { + "name": "funding_request", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0.0'" + }, + "data": { + "name": "data", + "type": "jsonb", + "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": { + "options_user_id_users_id_fk": { + "name": "options_user_id_users_id_fk", + "tableFrom": "options", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_registration_id_registrations_id_fk": { + "name": "options_registration_id_registrations_id_fk", + "tableFrom": "options", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_question_id_questions_id_fk": { + "name": "options_question_id_questions_id_fk", + "tableFrom": "options", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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_options_id_fk": { + "name": "votes_option_id_options_id_fk", + "tableFrom": "votes", + "tableTo": "options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_question_id_questions_id_fk": { + "name": "votes_question_id_questions_id_fk", + "tableFrom": "votes", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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 + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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" + ] + } + } + }, + "public.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": {} + }, + "public.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_questions_id_fk": { + "name": "questions_to_group_categories_question_id_questions_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "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/0029_snapshot.json b/migrations/meta/0029_snapshot.json new file mode 100644 index 00000000..dca61817 --- /dev/null +++ b/migrations/meta/0029_snapshot.json @@ -0,0 +1,1692 @@ +{ + "id": "ce83aad2-50c3-4f21-9fd5-0d684d95e87c", + "prevId": "3b3f3fe7-aab6-4ea0-88cd-e1c81a678986", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.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": {} + }, + "public.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 + }, + "option_id": { + "name": "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_option_id_options_id_fk": { + "name": "comments_option_id_options_id_fk", + "tableFrom": "comments", + "tableTo": "options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.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 + }, + "require_approval": { + "name": "require_approval", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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 + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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": {} + }, + "public.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" + ] + } + } + }, + "public.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 + }, + "required": { + "name": "required", + "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": {} + }, + "public.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": 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": { + "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" + ] + } + } + }, + "public.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" + ] + } + } + }, + "public.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'" + }, + "data": { + "name": "data", + "type": "jsonb", + "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": { + "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": {} + }, + "public.questions": { + "name": "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 + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "sub_title": { + "name": "sub_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "vote_model": { + "name": "vote_model", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "default": "'COCM'" + }, + "show_score": { + "name": "show_score", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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_cycle_id_cycles_id_fk": { + "name": "questions_cycle_id_cycles_id_fk", + "tableFrom": "questions", + "tableTo": "cycles", + "columnsFrom": [ + "cycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.options": { + "name": "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 + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "sub_title": { + "name": "sub_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "show": { + "name": "show", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vote_score": { + "name": "vote_score", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0.0'" + }, + "funding_request": { + "name": "funding_request", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0.0'" + }, + "data": { + "name": "data", + "type": "jsonb", + "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": { + "options_user_id_users_id_fk": { + "name": "options_user_id_users_id_fk", + "tableFrom": "options", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_registration_id_registrations_id_fk": { + "name": "options_registration_id_registrations_id_fk", + "tableFrom": "options", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_group_id_groups_id_fk": { + "name": "options_group_id_groups_id_fk", + "tableFrom": "options", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_question_id_questions_id_fk": { + "name": "options_question_id_questions_id_fk", + "tableFrom": "options", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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_options_id_fk": { + "name": "votes_option_id_options_id_fk", + "tableFrom": "votes", + "tableTo": "options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_question_id_questions_id_fk": { + "name": "votes_question_id_questions_id_fk", + "tableFrom": "votes", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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 + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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" + ] + } + } + }, + "public.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": {} + }, + "public.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_questions_id_fk": { + "name": "questions_to_group_categories_question_id_questions_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "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 6cb5fc89..fdccc044 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -197,6 +197,20 @@ "when": 1720035204142, "tag": "0027_stormy_silverclaw", "breakpoints": true + }, + { + "idx": 28, + "version": "6", + "when": 1720187374529, + "tag": "0028_green_gamora", + "breakpoints": true + }, + { + "idx": 29, + "version": "6", + "when": 1720190747562, + "tag": "0029_colorful_psynapse", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/events.ts b/src/db/events.ts index 5d59ea29..4cd45ca2 100644 --- a/src/db/events.ts +++ b/src/db/events.ts @@ -1,7 +1,8 @@ import { relations } from 'drizzle-orm'; -import { pgTable, timestamp, uuid, varchar, integer, boolean } from 'drizzle-orm/pg-core'; +import { pgTable, timestamp, uuid, varchar, integer, boolean, jsonb } from 'drizzle-orm/pg-core'; import { registrations } from './registrations'; -import { cycles, registrationFields } from '.'; +import { cycles } from './cycles'; +import { registrationFields } from './registration-fields'; export const events = pgTable('events', { id: uuid('id').primaryKey().defaultRandom(), @@ -10,6 +11,7 @@ export const events = pgTable('events', { description: varchar('description'), link: varchar('link'), registrationDescription: varchar('registration_description'), + fields: jsonb('fields').notNull().default([]), imageUrl: varchar('image_url'), eventDisplayRank: integer('event_display_rank'), createdAt: timestamp('created_at').notNull().defaultNow(), diff --git a/src/db/groups.ts b/src/db/groups.ts index c761a0d7..a9397db8 100644 --- a/src/db/groups.ts +++ b/src/db/groups.ts @@ -3,6 +3,7 @@ import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import { usersToGroups } from './users-to-groups'; import { groupCategories } from './group-categories'; import { registrations } from './registrations'; +import { options } from './options'; export const groups = pgTable('groups', { id: uuid('id').primaryKey().defaultRandom(), @@ -22,6 +23,7 @@ export const groupsRelations = relations(groups, ({ one, many }) => ({ references: [groupCategories.id], }), registrations: many(registrations), + options: many(options), usersToGroups: many(usersToGroups), })); diff --git a/src/db/options.ts b/src/db/options.ts index 4fd771e3..36bab029 100644 --- a/src/db/options.ts +++ b/src/db/options.ts @@ -1,23 +1,26 @@ -import { boolean, pgTable, timestamp, uuid, varchar, numeric } from 'drizzle-orm/pg-core'; +import { boolean, pgTable, timestamp, uuid, varchar, numeric, jsonb } from 'drizzle-orm/pg-core'; import { questions } from './questions'; import { relations } from 'drizzle-orm'; import { votes } from './votes'; import { registrations } from './registrations'; import { comments } from './comments'; import { users } from './users'; +import { groups } from './groups'; export const options = pgTable('options', { id: uuid('id').primaryKey().defaultRandom(), userId: uuid('user_id').references(() => users.id), registrationId: uuid('registration_id').references(() => registrations.id), + groupId: uuid('group_id').references(() => groups.id), questionId: uuid('question_id') .references(() => questions.id) .notNull(), title: varchar('title', { length: 256 }).notNull(), subTitle: varchar('sub_title'), - accepted: boolean('accepted').default(false), + show: boolean('show').default(false), voteScore: numeric('vote_score').notNull().default('0.0'), fundingRequest: numeric('funding_request').default('0.0'), + data: jsonb('data'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); @@ -35,6 +38,10 @@ export const questionOptionsRelations = relations(options, ({ one, many }) => ({ fields: [options.registrationId], references: [registrations.id], }), + group: one(groups, { + fields: [options.groupId], + references: [groups.id], + }), comment: many(comments), votes: many(votes), })); diff --git a/src/db/questions.ts b/src/db/questions.ts index 97530241..bb48ad14 100644 --- a/src/db/questions.ts +++ b/src/db/questions.ts @@ -1,4 +1,4 @@ -import { boolean, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; +import { boolean, jsonb, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import { cycles } from './cycles'; import { relations } from 'drizzle-orm'; import { options } from './options'; @@ -13,6 +13,7 @@ export const questions = pgTable('questions', { subTitle: varchar('sub_title', { length: 256 }), voteModel: varchar('vote_model', { length: 256 }).notNull().default('COCM'), showScore: boolean('show_score').default(false), + fields: jsonb('fields').notNull().default([]), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); diff --git a/src/db/registrations.ts b/src/db/registrations.ts index db93020d..d69e9979 100644 --- a/src/db/registrations.ts +++ b/src/db/registrations.ts @@ -1,5 +1,5 @@ import { relations } from 'drizzle-orm'; -import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; +import { jsonb, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import { events } from './events'; import { registrationData } from './registration-data'; import { users } from './users'; @@ -16,6 +16,7 @@ export const registrations = pgTable('registrations', { groupId: uuid('group_id').references(() => groups.id), // CAN BE: DRAFT, APPROVED, REJECTED AND MORE status: varchar('status').default('DRAFT'), + data: jsonb('data'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); diff --git a/src/handlers/cycles.ts b/src/handlers/cycles.ts index 4e383f33..b9dcb2c4 100644 --- a/src/handlers/cycles.ts +++ b/src/handlers/cycles.ts @@ -15,7 +15,7 @@ export function getActiveCyclesHandler(dbPool: NodePgDatabase) { columns: { voteScore: false, }, - where: eq(db.options.accepted, true), + where: eq(db.options.show, true), }, }, }, diff --git a/src/handlers/events.ts b/src/handlers/events.ts index 743b9069..85a26122 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -20,7 +20,7 @@ export function getEventCyclesHandler(dbPool: NodePgDatabase) { columns: { voteScore: false, }, - where: eq(db.options.accepted, true), + where: eq(db.options.show, true), }, }, }, diff --git a/src/handlers/registrations.ts b/src/handlers/registrations.ts index 8b882f73..4768bdf4 100644 --- a/src/handlers/registrations.ts +++ b/src/handlers/registrations.ts @@ -1,12 +1,12 @@ import type { Request, Response } from 'express'; import * as db from '../db'; import { insertRegistrationSchema } from '../types'; -import { validateRequiredRegistrationFields } from '../services/registration-fields'; import { saveRegistration, updateRegistration, - validateCreateRegistrationPermissions, - validateUpdateRegistrationPermissions, + validateUpdateRegistrationAuthorization, + validateCreateRegistrationAuthorization, + validateEventFields, } from '../services/registrations'; import { eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; @@ -50,18 +50,16 @@ export function saveRegistrationHandler(dbPool: NodePgDatabase) { return res.status(400).json({ errors: body.error.issues }); } - const missingRequiredFields = await validateRequiredRegistrationFields({ + const brokenRules = await validateEventFields({ dbPool, - data: body.data, - forGroup: !!body.data.groupId, - forUser: !body.data.groupId, + registration: body.data, }); - if (missingRequiredFields.length > 0) { - return res.status(400).json({ errors: missingRequiredFields }); + if (brokenRules.length > 0) { + return res.status(400).json({ errors: brokenRules }); } - const canRegisterGroup = await validateCreateRegistrationPermissions({ + const canRegisterGroup = await validateCreateRegistrationAuthorization({ dbPool, userId, groupId: body.data.groupId, @@ -97,18 +95,16 @@ export function updateRegistrationHandler(dbPool: NodePgDatabase) { return res.status(400).json({ errors: body.error.issues }); } - const missingRequiredFields = await validateRequiredRegistrationFields({ + const brokenRules = await validateEventFields({ dbPool, - data: body.data, - forGroup: !!body.data.groupId, - forUser: !body.data.groupId, + registration: body.data, }); - if (missingRequiredFields.length > 0) { - return res.status(400).json({ errors: missingRequiredFields }); + if (brokenRules.length > 0) { + return res.status(400).json({ errors: brokenRules }); } - const canUpdateRegistration = await validateUpdateRegistrationPermissions({ + const canUpdateRegistration = await validateUpdateRegistrationAuthorization({ dbPool, registrationId, userId, diff --git a/src/services/cycles.ts b/src/services/cycles.ts index 5825fac4..a4d98707 100644 --- a/src/services/cycles.ts +++ b/src/services/cycles.ts @@ -27,7 +27,7 @@ export async function GetCycleById(dbPool: NodePgDatabase, cycleId: s }, }, }, - where: eq(db.options.accepted, true), + where: eq(db.options.show, true), }, }, }, @@ -42,7 +42,7 @@ export async function GetCycleById(dbPool: NodePgDatabase, cycleId: s questionOptions: question.options.map((option) => { return { id: option.id, - accepted: option.accepted, + show: option.show, title: option.title, subTitle: option.subTitle, questionId: option.questionId, diff --git a/src/services/forum-questions.ts b/src/services/forum-questions.ts index e234b7f4..2325e24c 100644 --- a/src/services/forum-questions.ts +++ b/src/services/forum-questions.ts @@ -1,6 +1,10 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import * as db from '../db'; -import { sql } from 'drizzle-orm'; +import { eq, sql } from 'drizzle-orm'; +import { z } from 'zod'; +import { insertOptionsSchema } from '../types/options'; +import { fieldsSchema } from '../types'; +import { enforceRules } from './validation'; export function availableHearts( numProposals: number, @@ -71,3 +75,38 @@ export async function getQuestionHearts( return 0; } } + +export async function validateQuestionFields({ + option, + dbPool, +}: { + dbPool: NodePgDatabase; + option: z.infer; +}) { + const rows = await dbPool + .select() + .from(db.questions) + .where(eq(db.questions.id, option.questionId)); + + if (!rows.length) { + return []; + } + + const question = rows[0]; + + if (!question) { + return []; + } + + // get registration fields for the event + const questionFields = fieldsSchema.safeParse(question.fields); + + if (!questionFields.success) { + return []; + } + + return enforceRules({ + data: option.data, + fields: questionFields.data, + }); +} diff --git a/src/services/registration-data.spec.ts b/src/services/registration-data.spec.ts deleted file mode 100644 index 93b19456..00000000 --- a/src/services/registration-data.spec.ts +++ /dev/null @@ -1,130 +0,0 @@ -import * as db from '../db'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; -import { environmentVariables, insertRegistrationSchema } from '../types'; -import { cleanup, seed } from '../utils/db/seed'; -import { z } from 'zod'; -import { upsertRegistrationData } from './registration-data'; -import { saveRegistration } from './registrations'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; - -describe('service: registrationData', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; - let registrationField: db.RegistrationField | undefined; - let registration: db.Registration | undefined; - let testRegistration: z.infer; - - beforeAll(async () => { - const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - dbPool = initDb.db; - dbConnection = initDb.client; - // seed - const { events, users, registrationFields } = await seed(dbPool); - - registrationField = registrationFields[0]; - - 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', - }, - ], - }; - - // Add test registration data to the db - registration = await saveRegistration(dbPool, testRegistration); - }); - - test('should update existing records', async () => { - // Call the function with registration ID and registration data to update - const registrationId = registration?.id ?? ''; - const registrationFieldId = registrationField?.id ?? ''; - const updatedValue = 'updated'; - - const registrationTestData = [ - { - registrationFieldId: registrationFieldId, - value: updatedValue, - }, - ]; - - const updatedData = await upsertRegistrationData({ - dbPool, - registrationId: registrationId, - registrationData: registrationTestData, - }); - - // Assert that the updated data array is not empty - expect(updatedData).toBeDefined(); - expect(updatedData).not.toBeNull(); - - if (updatedData) { - // Assert that the updated data has the correct structure - expect(updatedData.length).toBeGreaterThan(0); - expect(updatedData[0]).toHaveProperty('id'); - expect(updatedData[0]).toHaveProperty('registrationId', registrationId); - expect(updatedData[0]).toHaveProperty('registrationFieldId', registrationFieldId); - expect(updatedData[0]).toHaveProperty('value', updatedValue); - expect(updatedData[0]).toHaveProperty('createdAt'); - expect(updatedData[0]).toHaveProperty('updatedAt'); - } - }); - - test('should return null when an error occurs', async () => { - // Provide an invalid registration id to trigger the error - const registrationId = ''; - const registrationFieldId = registrationField?.id ?? ''; - const updatedValue = 'updated'; - - const registrationTestData = [ - { - registrationFieldId: registrationFieldId, - value: updatedValue, - }, - ]; - - const updatedData = await upsertRegistrationData({ - dbPool, - registrationId: registrationId, - registrationData: registrationTestData, - }); - - // Assert that the function returns null when an error occurs - expect(updatedData).toBeNull(); - }); - - afterAll(async () => { - await cleanup(dbPool); - await dbConnection.end(); - }); -}); diff --git a/src/services/registration-data.ts b/src/services/registration-data.ts deleted file mode 100644 index 721fb774..00000000 --- a/src/services/registration-data.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import * as db from '../db'; -import { eq, and } from 'drizzle-orm'; - -/** - * Upserts the registration data for a given registrationId. - * Updates existing records and inserts new ones if necessary. - * @param dbPool - The database pool instance of type ` NodePgDatabase`. - * @param registrationId - The ID of the registration to overwrite data for. - * @param registrationData - An array of objects representing registration data. - * Each object should have the properties: - * - registrationFieldId: The identifier for the registration field associated with the data. - * - value: The value of the registration data. - * @returns A Promise that resolves to the updated registration data or null if an error occurs. - */ -export async function upsertRegistrationData({ - dbPool, - registrationId, - registrationData, -}: { - dbPool: NodePgDatabase; - registrationId: string; - registrationData: { - registrationFieldId: string; - value: string; - }[]; -}): Promise { - try { - const updatedRegistrationData: db.RegistrationData[] = []; - - for (const data of registrationData) { - // Find the existing record - const existingRecord = await dbPool.query.registrationData.findFirst({ - where: and( - eq(db.registrationData.registrationId, registrationId), - eq(db.registrationData.registrationFieldId, data.registrationFieldId), - ), - }); - - if (existingRecord) { - // If the record exists, update it - await dbPool - .update(db.registrationData) - .set({ value: data.value, updatedAt: new Date() }) - .where(and(eq(db.registrationData.id, existingRecord.id))); - - // Push the updated record into the array - updatedRegistrationData.push({ ...existingRecord, value: data.value }); - } else { - // If the record doesn't exist, insert a new one - const insertedRecord = await dbPool - .insert(db.registrationData) - .values({ - registrationId, - registrationFieldId: data.registrationFieldId, - value: data.value, - }) - .returning(); - - if (insertedRecord?.[0]) { - updatedRegistrationData.push(insertedRecord?.[0]); - } - } - } - - return updatedRegistrationData; - } catch (e) { - console.log('Error updating/inserting registration data ' + JSON.stringify(e)); - return null; - } -} diff --git a/src/services/registration-fields.spec.ts b/src/services/registration-fields.spec.ts deleted file mode 100644 index 423d2463..00000000 --- a/src/services/registration-fields.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { z } from 'zod'; -import * as db from '../db'; -import { environmentVariables, insertRegistrationSchema } from '../types'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; -import { cleanup, seed } from '../utils/db/seed'; -import { validateRequiredRegistrationFields } from './registration-fields'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; - -describe('service: registrationFields', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; - let requiredByGroupRegistrationField: db.RegistrationField | undefined; - let requiredByUserRegistrationField: db.RegistrationField | undefined; - let testRegistration: z.infer; - - beforeAll(async () => { - const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - dbPool = initDb.db; - dbConnection = initDb.client; - // 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/registration-fields.ts b/src/services/registration-fields.ts deleted file mode 100644 index a3481e70..00000000 --- a/src/services/registration-fields.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import * as db from '../db'; -import { and, eq } from 'drizzle-orm'; - -export async function validateRequiredRegistrationFields({ - data, - dbPool, - forGroup, - forUser, -}: { - dbPool: NodePgDatabase; - 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: { - where: and( - eq(db.registrationFields.forUser, forUser), - eq(db.registrationFields.forGroup, forGroup), - eq(db.registrationFields.required, true), - ), - }, - }, - where: eq(db.events.id, data.eventId), - }); - const requiredFields = event?.registrationFields; - - if (!requiredFields) { - return []; - } - - // loop through required fields and check if they are filled - 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, - message: 'missing required field', - })); -} diff --git a/src/services/registrations.spec.ts b/src/services/registrations.spec.ts index febe639c..1d4f54ea 100644 --- a/src/services/registrations.spec.ts +++ b/src/services/registrations.spec.ts @@ -1,18 +1,21 @@ import * as db from '../db'; import { createDbClient } from '../utils/db/create-db-connection'; import { runMigrations } from '../utils/db/run-migrations'; -import { saveRegistration } from './registrations'; -import { z } from 'zod'; -import { environmentVariables, insertRegistrationSchema } from '../types'; +import { + validateCreateRegistrationAuthorization, + validateUpdateRegistrationAuthorization, +} from './registrations'; +import { environmentVariables } from '../types'; import { cleanup, seed } from '../utils/db/seed'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { Client } from 'pg'; +import { eq } from 'drizzle-orm'; describe('service: registrations', () => { let dbPool: NodePgDatabase; let dbConnection: Client; - let testData: z.infer; - + let userId: string; + let eventId: string; beforeAll(async () => { const envVariables = environmentVariables.parse(process.env); const initDb = await createDbClient({ @@ -34,73 +37,104 @@ describe('service: registrations', () => { dbPool = initDb.db; dbConnection = initDb.client; // seed - const { events, registrationFields, users } = await seed(dbPool); - // Insert registration fields for the user - testData = { - userId: users[0]?.id ?? '', - eventId: events[0]?.id ?? '', - status: 'DRAFT', - registrationData: [ - { - registrationFieldId: registrationFields[0]?.id ?? '', - value: 'option2', - }, - ], - }; + const { users, events } = await seed(dbPool); + userId = users?.[0]?.id ?? ''; + eventId = events?.[0]?.id ?? ''; }); - test('send registration data', async function () { - // Call the saveRegistration function - const response = await saveRegistration(dbPool, testData); - // Check if response is defined - expect(response).toBeDefined(); - // Check property existence and types - expect(response).toHaveProperty('id'); - expect(response.id).toEqual(expect.any(String)); - expect(response).toHaveProperty('userId'); - expect(response.userId).toEqual(expect.any(String)); - // check registration data - expect(response.registrationData).toEqual(expect.any(Array)); - expect(response.registrationData).toHaveLength(1); - // Check array element properties - response.registrationData!.forEach((data) => { - expect(data).toHaveProperty('value'); - expect(data).toHaveProperty('registrationFieldId'); + + describe('validate: create registration authorization', function () { + test('when the user is not in the group', async function () { + const notRealGroupId = userId; + + const result = await validateCreateRegistrationAuthorization({ + dbPool, + userId, + groupId: notRealGroupId, + }); + + expect(result).toBe(false); + }); + test('when the user is in the group', async function () { + const group = await dbPool.query.usersToGroups.findFirst({ + where: eq(db.usersToGroups.userId, userId), + }); + + if (!group) { + throw new Error('No group found'); + } + + const result = await validateCreateRegistrationAuthorization({ + dbPool, + userId, + groupId: group.groupId, + }); + + expect(result).toBe(true); }); - // check timestamps - expect(response.createdAt).toEqual(expect.any(Date)); - expect(response.updatedAt).toEqual(expect.any(Date)); }); - test('update registration data', async function () { - // update testData - testData.registrationData = [ - { - registrationFieldId: testData.registrationData[0]?.registrationFieldId ?? '', - value: 'updated', - }, - ]; - // Call the saveRegistration function - const response = await saveRegistration(dbPool, testData); - // Check if response is defined - expect(response).toBeDefined(); - // Check property existence and types - expect(response).toHaveProperty('id'); - expect(response.id).toEqual(expect.any(String)); - expect(response).toHaveProperty('userId'); - expect(response.userId).toEqual(expect.any(String)); - // check registration data - expect(response.registrationData).toEqual(expect.any(Array)); - expect(response.registrationData).toHaveLength(1); - // Check array element properties - response.registrationData!.forEach((data) => { - expect(data).toHaveProperty('value'); - expect(data).toHaveProperty('registrationFieldId'); + + describe('validate: update registration authorization', function () { + test('when the user is not in the group', async function () { + const rows = await dbPool + .insert(db.registrations) + .values({ + eventId: eventId, + userId: userId, + }) + .returning(); + + if (!rows) { + throw new Error('No registration found'); + } + + if (!rows[0]) { + throw new Error('No registration found'); + } + + const notRealGroupId = userId; + const result = await validateUpdateRegistrationAuthorization({ + dbPool, + userId, + groupId: notRealGroupId, + registrationId: rows[0].id, + }); + + expect(result).toBe(false); }); - // check timestamps - expect(response.createdAt).toEqual(expect.any(Date)); - expect(response.updatedAt).toEqual(expect.any(Date)); + test('when the user is in the group', async function () { + const rows = await dbPool + .insert(db.registrations) + .values({ + eventId: eventId, + userId: userId, + }) + .returning(); + + if (!rows) { + throw new Error('No registration found'); + } + + if (!rows[0]) { + throw new Error('No registration found'); + } - // Check if the value was updated - expect(response.registrationData?.[0]?.value).toEqual('updated'); + const userGroup = await dbPool.query.usersToGroups.findFirst({ + where: eq(db.usersToGroups.userId, userId), + }); + + if (!userGroup) { + throw new Error('No group found'); + } + + const result = await validateUpdateRegistrationAuthorization({ + dbPool, + userId, + groupId: userGroup.groupId, + registrationId: rows[0].id, + }); + + expect(result).toBe(true); + }); }); afterAll(async () => { // Delete registration data diff --git a/src/services/registrations.ts b/src/services/registrations.ts index f0f76acb..16527258 100644 --- a/src/services/registrations.ts +++ b/src/services/registrations.ts @@ -1,11 +1,11 @@ import { and, eq } from 'drizzle-orm'; import { z } from 'zod'; -import { insertRegistrationSchema } from '../types'; +import { insertRegistrationSchema, fieldsSchema } from '../types'; import * as db from '../db'; -import { upsertRegistrationData } from './registration-data'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { enforceRules } from './validation'; -export async function validateCreateRegistrationPermissions({ +export async function validateCreateRegistrationAuthorization({ dbPool, userId, groupId, @@ -15,7 +15,7 @@ export async function validateCreateRegistrationPermissions({ groupId?: string | null; }) { if (groupId) { - const userGroup = dbPool.query.usersToGroups.findFirst({ + const userGroup = await dbPool.query.usersToGroups.findFirst({ where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, groupId)), }); @@ -27,7 +27,7 @@ export async function validateCreateRegistrationPermissions({ return true; } -export async function validateUpdateRegistrationPermissions({ +export async function validateUpdateRegistrationAuthorization({ dbPool, registrationId, userId, @@ -51,7 +51,7 @@ export async function validateUpdateRegistrationPermissions({ } if (groupId) { - const userGroup = dbPool.query.usersToGroups.findFirst({ + const userGroup = await dbPool.query.usersToGroups.findFirst({ where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, groupId)), }); @@ -63,16 +63,52 @@ export async function validateUpdateRegistrationPermissions({ return true; } +export async function validateEventFields({ + registration, + dbPool, +}: { + dbPool: NodePgDatabase; + registration: z.infer; +}) { + const rows = await dbPool.select().from(db.events).where(eq(db.events.id, registration.eventId)); + + if (!rows.length) { + return []; + } + + const event = rows[0]; + + if (!event) { + return []; + } + + // get fields for the event + const eventFields = fieldsSchema.safeParse(event.fields); + + if (!eventFields.success) { + return []; + } + + return enforceRules({ + data: registration.data, + fields: eventFields.data, + }); +} + export async function saveRegistration( dbPool: NodePgDatabase, - data: z.infer, + registration: z.infer, ) { const event = await dbPool.query.events.findFirst({ - where: eq(db.events.id, data.eventId), + where: eq(db.events.id, registration.eventId), }); + if (!event) { + throw new Error('event not found'); + } + const newRegistration = await createRegistrationInDB(dbPool, { - ...data, + ...registration, status: event?.requireApproval ? 'DRAFT' : 'APPROVED', }); @@ -80,19 +116,8 @@ export async function saveRegistration( throw new Error('failed to save registration'); } - const updatedRegistrationData = await upsertRegistrationData({ - dbPool, - registrationId: newRegistration.id, - registrationData: data.registrationData, - }); - - if (!updatedRegistrationData) { - throw new Error('Failed to upsert registration data'); - } - const out = { ...newRegistration, - registrationData: updatedRegistrationData, }; return out; @@ -123,19 +148,8 @@ export async function updateRegistration({ throw new Error('failed to save registration'); } - const updatedRegistrationData = await upsertRegistrationData({ - dbPool, - registrationId: updatedRegistration.id, - registrationData: data.registrationData, - }); - - if (!updatedRegistrationData) { - throw new Error('Failed to upsert registration data'); - } - const out = { ...updatedRegistration, - registrationData: updatedRegistrationData, }; return out; @@ -152,6 +166,7 @@ async function createRegistrationInDB( userId: body.userId, groupId: body.groupId, eventId: body.eventId, + data: body.data, status: body.status, }) .returning(); @@ -168,6 +183,7 @@ async function updateRegistrationInDB( .set({ eventId: body.eventId, groupId: body.groupId, + data: body.data, updatedAt: new Date(), }) .where(eq(db.registrations.id, registration.id)) diff --git a/src/services/statistics.ts b/src/services/statistics.ts index 1fc476fd..e6c6af70 100644 --- a/src/services/statistics.ts +++ b/src/services/statistics.ts @@ -48,7 +48,7 @@ export async function executeResultQueries( SELECT count("id")::int AS "numProposals" FROM options WHERE question_id = '${forumQuestionId}' - AND accepted = TRUE + AND show = TRUE `), ), @@ -121,7 +121,7 @@ export async function executeResultQueries( SELECT "id" AS "optionId", "title" AS "title", "sub_title" AS "subTitle", vote_score AS "pluralityScore" FROM options WHERE question_id = '${forumQuestionId}' - AND accepted = TRUE -- makes sure to only expose data of accepted options + AND show = TRUE -- makes sure to only expose data of accepted options ), allocated_hearts AS ( diff --git a/src/services/validation.spec.ts b/src/services/validation.spec.ts new file mode 100644 index 00000000..8c3be69f --- /dev/null +++ b/src/services/validation.spec.ts @@ -0,0 +1,382 @@ +import { z } from 'zod'; +import { dataSchema, fieldsSchema } from '../types'; +import { enforceRules } from './validation'; + +describe('service: validation', function () { + describe('rule: required', function () { + test('should return an error if a required field is missing', function () { + const fields: z.infer = [ + { + id: 'name', + name: 'Name', + position: 1, + type: 'TEXT', + validation: { + required: true, + }, + }, + ]; + const data: z.infer = {}; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(1); + expect(result).toEqual(['Name is required']); + }); + + test('should not return an error if a required field is present', function () { + const fields: z.infer = [ + { + id: 'name', + name: 'Name', + position: 1, + type: 'TEXT', + validation: { + required: true, + }, + }, + ]; + const data: z.infer = { + name: { + value: 'John Doe', + fieldId: 'name', + type: 'TEXT', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(0); + }); + }); + + describe('officer: string', function () { + describe('rule: minLength', function () { + test('should return an error if the string is too short', function () { + const fields: z.infer = [ + { + id: 'name', + name: 'Name', + position: 1, + type: 'TEXT', + validation: { + required: true, + minLength: 5, + }, + }, + ]; + const data: z.infer = { + name: { + value: 'John', + fieldId: 'name', + type: 'TEXT', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(1); + expect(result).toEqual(['Name must be at least 5 characters']); + }); + + test('should not return an error if the string is long enough', function () { + const fields: z.infer = [ + { + id: 'name', + name: 'Name', + position: 1, + type: 'TEXT', + validation: { + required: true, + minLength: 5, + }, + }, + ]; + const data: z.infer = { + name: { + value: 'John Doe', + fieldId: 'name', + type: 'TEXT', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(0); + }); + }); + describe('rule: maxLength', function () { + test('should return an error if the string is too long', function () { + const fields: z.infer = [ + { + id: 'name', + name: 'Name', + position: 1, + type: 'TEXT', + validation: { + required: true, + maxLength: 5, + }, + }, + ]; + const data: z.infer = { + name: { + value: 'John Doe', + fieldId: 'name', + type: 'TEXT', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(1); + expect(result).toEqual(['Name must be at most 5 characters']); + }); + + test('should not return an error if the string is short enough', function () { + const fields: z.infer = [ + { + id: 'name', + name: 'Name', + position: 1, + type: 'TEXT', + validation: { + required: true, + maxLength: 5, + }, + }, + ]; + const data: z.infer = { + name: { + value: 'John', + fieldId: 'name', + type: 'TEXT', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(0); + }); + }); + }); + + describe('officer: number', function () { + describe('rule: minLength', function () { + test('should return an error if the number is too small', function () { + const fields: z.infer = [ + { + id: 'age', + name: 'Age', + position: 1, + type: 'NUMBER', + validation: { + required: true, + minLength: 18, + }, + }, + ]; + const data: z.infer = { + age: { + value: 17, + fieldId: 'age', + type: 'NUMBER', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(1); + expect(result).toEqual(['Age must be at least 18']); + }); + + test('should not return an error if the number is large enough', function () { + const fields: z.infer = [ + { + id: 'age', + name: 'Age', + position: 1, + type: 'NUMBER', + validation: { + required: true, + minLength: 18, + }, + }, + ]; + const data: z.infer = { + age: { + value: 18, + fieldId: 'age', + type: 'NUMBER', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(0); + }); + }); + describe('rule: maxLength', function () { + test('should return an error if the number is too large', function () { + const fields: z.infer = [ + { + id: 'age', + name: 'Age', + position: 1, + type: 'NUMBER', + validation: { + required: true, + maxLength: 18, + }, + }, + ]; + const data: z.infer = { + age: { + value: 19, + fieldId: 'age', + type: 'NUMBER', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(1); + expect(result).toEqual(['Age must be at most 18']); + }); + test('should not return an error if the number is small enough', function () { + const fields: z.infer = [ + { + id: 'age', + name: 'Age', + position: 1, + type: 'NUMBER', + validation: { + required: true, + maxLength: 18, + }, + }, + ]; + const data: z.infer = { + age: { + value: 18, + fieldId: 'age', + type: 'NUMBER', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(0); + }); + }); + }); + + describe('officer: array', function () { + describe('rule: minLength', function () { + test('should return an error if the array is too small', function () { + const fields: z.infer = [ + { + id: 'colors', + name: 'Colors', + position: 1, + type: 'MULTI_SELECT', + validation: { + required: true, + minLength: 2, + }, + }, + ]; + const data: z.infer = { + colors: { + value: ['red'], + fieldId: 'colors', + type: 'MULTI_SELECT', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(1); + expect(result).toEqual(['Colors must have at least 2 items']); + }); + test('should not return an error if the array is large enough', function () { + const fields: z.infer = [ + { + id: 'colors', + name: 'Colors', + position: 1, + type: 'MULTI_SELECT', + validation: { + required: true, + minLength: 2, + }, + }, + ]; + const data: z.infer = { + colors: { + value: ['red', 'blue'], + fieldId: 'colors', + type: 'MULTI_SELECT', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(0); + }); + }); + describe('rule: maxLength', function () { + test('should return an error if the array is too large', function () { + const fields: z.infer = [ + { + id: 'colors', + name: 'Colors', + position: 1, + type: 'MULTI_SELECT', + validation: { + required: true, + maxLength: 2, + }, + }, + ]; + const data: z.infer = { + colors: { + value: ['red', 'blue', 'green'], + fieldId: 'colors', + type: 'MULTI_SELECT', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(1); + expect(result).toEqual(['Colors must have at most 2 items']); + }); + test('should not return an error if the array is small enough', function () { + const fields: z.infer = [ + { + id: 'colors', + name: 'Colors', + position: 1, + type: 'MULTI_SELECT', + validation: { + required: true, + maxLength: 2, + }, + }, + ]; + const data: z.infer = { + colors: { + value: ['red', 'blue'], + fieldId: 'colors', + type: 'MULTI_SELECT', + }, + }; + + const result = enforceRules({ data, fields }); + + expect(result.length).toBe(0); + }); + }); + }); +}); diff --git a/src/services/validation.ts b/src/services/validation.ts new file mode 100644 index 00000000..3e74d9fe --- /dev/null +++ b/src/services/validation.ts @@ -0,0 +1,116 @@ +import { z } from 'zod'; +import { dataSchema, fieldsSchema } from '../types'; + +type OfficerResponse = string | null; + +export function enforceRules({ + data, + fields, +}: { + fields: z.infer | undefined | null; + data: z.infer | undefined | null; +}) { + const brokenRules = []; + + if (!fields) { + return []; + } + + for (const field of fields) { + const value = data?.[field.id]?.value; + + if (field.validation.required && !value) { + brokenRules.push(`${field.name} is required`); + } + + if (value === undefined || value === null) { + continue; + } + + switch (field.type) { + case 'TEXT': + case 'TEXTAREA': + case 'SELECT': { + const stringBrokenRule = stringOfficer({ field, value: value as string }); + if (stringBrokenRule) { + brokenRules.push(stringBrokenRule); + } + break; + } + case 'NUMBER': { + const numBrokenRule = numberOfficer({ field, value: value as number }); + if (numBrokenRule) { + brokenRules.push(numBrokenRule); + } + break; + } + + case 'MULTI_SELECT': { + const arrayBrokenRule = arrayOfficer({ field, value: value as string[] }); + if (arrayBrokenRule) { + brokenRules.push(arrayBrokenRule); + } + break; + } + + default: + break; + } + } + + return brokenRules; +} + +export function stringOfficer({ + field, + value, +}: { + value: string; + field: z.infer[number]; +}): OfficerResponse { + if (field.validation.minLength && value.length < field.validation.minLength) { + return `${field.name} must be at least ${field.validation.minLength} characters`; + } + + if (field.validation.maxLength && value.length > field.validation.maxLength) { + return `${field.name} must be at most ${field.validation.maxLength} characters`; + } + + return null; +} + +export function numberOfficer({ + field, + value, +}: { + value: number; + field: z.infer[number]; +}): OfficerResponse { + if (field.validation.minLength && value < field.validation.minLength) { + return `${field.name} must be at least ${field.validation.minLength}`; + } + + if (field.validation.maxLength && value > field.validation.maxLength) { + return `${field.name} must be at most ${field.validation.maxLength}`; + } + + return null; +} + +export function arrayOfficer({ + field, + value, +}: { + value: string[]; + field: z.infer[number]; +}): OfficerResponse { + if (field.validation.minLength && value.length < field.validation.minLength) { + return `${field.name} must have at least ${field.validation.minLength} items`; + } + + if (field.validation.maxLength && value.length > field.validation.maxLength) { + return `${field.name} must have at most ${field.validation.maxLength} items`; + } + + return null; +} diff --git a/src/types/index.ts b/src/types/index.ts index d5993744..95db893b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,3 +5,5 @@ export * from './users'; export * from './cycles'; export * from './auth'; export * from './comments'; +export * from './validation'; +export * from './options'; diff --git a/src/types/options.ts b/src/types/options.ts new file mode 100644 index 00000000..b9c7ccc7 --- /dev/null +++ b/src/types/options.ts @@ -0,0 +1,7 @@ +import { createInsertSchema } from 'drizzle-zod'; +import { dataSchema } from './validation'; +import { options } from '../db'; + +export const insertOptionsSchema = createInsertSchema(options, { + data: dataSchema, +}); diff --git a/src/types/registrations.ts b/src/types/registrations.ts index 635ed858..e0e0d6cf 100644 --- a/src/types/registrations.ts +++ b/src/types/registrations.ts @@ -1,17 +1,9 @@ import { createInsertSchema } from 'drizzle-zod'; import { registrations } from '../db/registrations'; -import { z } from 'zod'; +import { dataSchema } from './validation'; -// array of registration data -export const registrationDataSchema = z - .object({ - registrationFieldId: z.string(), - value: z.string(), - }) - .array(); - -export const insertRegistrationSchema = createInsertSchema(registrations).extend({ - registrationData: registrationDataSchema, +export const insertRegistrationSchema = createInsertSchema(registrations, { + data: dataSchema, }); export const insertSimpleRegistrationSchema = createInsertSchema(registrations); diff --git a/src/types/validation.ts b/src/types/validation.ts new file mode 100644 index 00000000..8107c4b5 --- /dev/null +++ b/src/types/validation.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; + +/** + * Fields schema + */ + +const fieldType = z.enum(['TEXT', 'TEXTAREA', 'SELECT', 'CHECKBOX', 'MULTI_SELECT', 'NUMBER']); + +export const fieldsSchema = z.array( + z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().optional(), + type: fieldType, + position: z.number(), + options: z.array(z.string()).optional(), + validation: z.object({ + required: z.boolean(), + minLength: z.number().optional(), + maxLength: z.number().optional(), + }), + }), +); + +/** + * Data Schema + */ + +const dataValueSchema = z.union([ + z.string(), + z.number(), + z.boolean(), + z.array(z.string()), // For multi-select fields + z.null(), // In case of optional fields +]); + +// Define a schema for a single field +const dataForOneFieldSchema = z.object({ + value: dataValueSchema, + fieldId: z.string().uuid(), + type: fieldType, +}); + +// [fieldId] => { value: [value], fieldId: [fieldId] } +export const dataSchema = z.record(z.string(), dataForOneFieldSchema); diff --git a/src/utils/db/seed-data-generators.ts b/src/utils/db/seed-data-generators.ts index 70b489f6..6f2b0c25 100644 --- a/src/utils/db/seed-data-generators.ts +++ b/src/utils/db/seed-data-generators.ts @@ -1,4 +1,4 @@ -import { randCompanyName, randCountry, randUser } from '@ngneat/falso'; +import { randCompanyName, randCountry, randUser, randUuid } from '@ngneat/falso'; import { Cycle, Event, @@ -12,10 +12,12 @@ import { UsersToGroups, QuestionsToGroupCategories, } from '../../db'; +import { z } from 'zod'; +import { fieldsSchema } from '../../types'; // Define types export type CycleData = Pick; -export type EventData = Pick; +export type EventData = Pick; export type RegistrationFieldData = Pick< RegistrationField, 'name' | 'eventId' | 'type' | 'required' | 'forUser' | 'forGroup' @@ -25,7 +27,7 @@ export type RegistrationFieldOptionData = Pick< 'registrationFieldId' | 'value' >; export type ForumQuestionData = Pick; -export type QuestionOptionData = Pick; +export type QuestionOptionData = Pick; export type GroupCategoryData = Pick< GroupCategory, 'name' | 'eventId' | 'userCanCreate' | 'userCanView' | 'required' @@ -40,8 +42,30 @@ export type QuestionsToGroupCategoriesData = Pick< export function generateEventData(numEvents: number): EventData[] { const events: EventData[] = []; + const fields: z.infer = [ + { + id: randUuid(), + name: 'What do you think about this event?', + position: 0, + type: 'TEXT', + validation: { + required: true, + }, + }, + { + id: randUuid(), + name: 'Opinion', + description: 'Optional opinion', + position: 1, + type: 'TEXT', + validation: { + required: false, + }, + }, + ]; + for (let i = 0; i < numEvents; i++) { - events.push({ name: randCountry() }); + events.push({ name: randCountry(), fields }); } return events; } @@ -113,7 +137,7 @@ export function generateQuestionOptionsData( const optionData: QuestionOptionData = { questionId, title: optionTitles[i]!, - accepted: status[i]!, + show: status[i]!, }; questionOptionsData.push(optionData); } diff --git a/src/utils/db/seed.ts b/src/utils/db/seed.ts index bf01456a..4b75b041 100644 --- a/src/utils/db/seed.ts +++ b/src/utils/db/seed.ts @@ -147,11 +147,12 @@ async function cleanup(dbPool: NodePgDatabase) { async function createEvent(dbPool: NodePgDatabase, eventData: EventData[]) { const events = []; - for (const eventName of eventData) { + for (const event of eventData) { const result = await dbPool .insert(db.events) .values({ - name: eventName.name, + name: event.name, + fields: event.fields, }) .returning(); events.push(result[0]); @@ -294,7 +295,7 @@ async function createQuestionOptions( .values({ questionId: questionOption.questionId, title: questionOption.title, - accepted: questionOption.accepted, + show: questionOption.show, }) .returning(); From 5576e46471fc165fa79286a7167c2a2b650a27e8 Mon Sep 17 00:00:00 2001 From: Martin Benedikt Busch <43137759+MartinBenediktBusch@users.noreply.github.com> Date: Tue, 9 Jul 2024 18:07:14 +0200 Subject: [PATCH 04/14] refactor vote service (#441) * make group category id required * add ts-docstring * avoid quering question id at each plural score calculation * separate vote validation from saving * separate validation and saving from updating the vote score * update tests * implemented suggestions * refactor updateOptionScore function * drop migration * add db migration * update tests and remove dependencies --- migrations/0030_polite_forge.sql | 1 + migrations/meta/0030_snapshot.json | 1692 +++++++++++++++++++++++ migrations/meta/_journal.json | 7 + src/db/questions-to-group-categories.ts | 4 +- src/handlers/votes.ts | 16 +- src/services/votes.spec.ts | 200 ++- src/services/votes.ts | 292 ++-- 7 files changed, 2043 insertions(+), 169 deletions(-) create mode 100644 migrations/0030_polite_forge.sql create mode 100644 migrations/meta/0030_snapshot.json diff --git a/migrations/0030_polite_forge.sql b/migrations/0030_polite_forge.sql new file mode 100644 index 00000000..5f4c0238 --- /dev/null +++ b/migrations/0030_polite_forge.sql @@ -0,0 +1 @@ +ALTER TABLE "questions_to_group_categories" ALTER COLUMN "group_category_id" SET NOT NULL; \ No newline at end of file diff --git a/migrations/meta/0030_snapshot.json b/migrations/meta/0030_snapshot.json new file mode 100644 index 00000000..aba98eb5 --- /dev/null +++ b/migrations/meta/0030_snapshot.json @@ -0,0 +1,1692 @@ +{ + "id": "02ccac96-115d-47d7-86c9-d6f3c50b3d06", + "prevId": "ce83aad2-50c3-4f21-9fd5-0d684d95e87c", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.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": {} + }, + "public.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 + }, + "option_id": { + "name": "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_option_id_options_id_fk": { + "name": "comments_option_id_options_id_fk", + "tableFrom": "comments", + "tableTo": "options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.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 + }, + "require_approval": { + "name": "require_approval", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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 + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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": {} + }, + "public.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" + ] + } + } + }, + "public.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 + }, + "required": { + "name": "required", + "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": {} + }, + "public.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": 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": { + "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" + ] + } + } + }, + "public.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" + ] + } + } + }, + "public.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'" + }, + "data": { + "name": "data", + "type": "jsonb", + "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": { + "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": {} + }, + "public.questions": { + "name": "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 + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "sub_title": { + "name": "sub_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "vote_model": { + "name": "vote_model", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "default": "'COCM'" + }, + "show_score": { + "name": "show_score", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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_cycle_id_cycles_id_fk": { + "name": "questions_cycle_id_cycles_id_fk", + "tableFrom": "questions", + "tableTo": "cycles", + "columnsFrom": [ + "cycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.options": { + "name": "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 + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "sub_title": { + "name": "sub_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "show": { + "name": "show", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vote_score": { + "name": "vote_score", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0.0'" + }, + "funding_request": { + "name": "funding_request", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0.0'" + }, + "data": { + "name": "data", + "type": "jsonb", + "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": { + "options_user_id_users_id_fk": { + "name": "options_user_id_users_id_fk", + "tableFrom": "options", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_registration_id_registrations_id_fk": { + "name": "options_registration_id_registrations_id_fk", + "tableFrom": "options", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_group_id_groups_id_fk": { + "name": "options_group_id_groups_id_fk", + "tableFrom": "options", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_question_id_questions_id_fk": { + "name": "options_question_id_questions_id_fk", + "tableFrom": "options", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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_options_id_fk": { + "name": "votes_option_id_options_id_fk", + "tableFrom": "votes", + "tableTo": "options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_question_id_questions_id_fk": { + "name": "votes_question_id_questions_id_fk", + "tableFrom": "votes", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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 + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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" + ] + } + } + }, + "public.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": {} + }, + "public.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": 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": { + "questions_to_group_categories_question_id_questions_id_fk": { + "name": "questions_to_group_categories_question_id_questions_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "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 fdccc044..3081ce70 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -211,6 +211,13 @@ "when": 1720190747562, "tag": "0029_colorful_psynapse", "breakpoints": true + }, + { + "idx": 30, + "version": "6", + "when": 1720536020805, + "tag": "0030_polite_forge", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/questions-to-group-categories.ts b/src/db/questions-to-group-categories.ts index 8ec5a880..f63660a9 100644 --- a/src/db/questions-to-group-categories.ts +++ b/src/db/questions-to-group-categories.ts @@ -8,7 +8,9 @@ export const questionsToGroupCategories = pgTable('questions_to_group_categories questionId: uuid('question_id') .notNull() .references(() => questions.id), - groupCategoryId: uuid('group_category_id').references(() => groupCategories.id), // Must be nullable (for now) because affiliation does not have a group category id. + groupCategoryId: uuid('group_category_id') + .notNull() + .references(() => groupCategories.id), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); diff --git a/src/handlers/votes.ts b/src/handlers/votes.ts index 0f43a755..58c81364 100644 --- a/src/handlers/votes.ts +++ b/src/handlers/votes.ts @@ -1,7 +1,7 @@ import type { Request, Response } from 'express'; import { z } from 'zod'; import * as db from '../db'; -import { saveVotes } from '../services/votes'; +import { validateAndSaveVotes, updateOptionScore } from '../services/votes'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; /** @@ -29,13 +29,19 @@ export function saveVotesHandler(dbPool: NodePgDatabase) { // Insert votes try { - const votes = await saveVotes(dbPool, reqBody.data, userId); + const voteResults = await validateAndSaveVotes(dbPool, reqBody.data, userId); - if (votes.errors && votes.errors.length > 0) { - return res.status(400).json({ errors: votes.errors }); + if (voteResults.errors && voteResults.errors.length > 0) { + return res.status(400).json({ errors: voteResults.errors }); } - return res.json({ data: votes.data }); + const optionScores = await updateOptionScore(dbPool, reqBody.data, voteResults.questionIds); + + if (optionScores.errors && optionScores.errors.length > 0) { + return res.status(400).json({ errors: optionScores.errors }); + } + + return res.json({ data: optionScores.data }); } catch (e) { console.error(`[ERROR] ${e}`); return res.status(500).json({ errors: e }); diff --git a/src/services/votes.spec.ts b/src/services/votes.spec.ts index 5f7e75ba..4401e996 100644 --- a/src/services/votes.spec.ts +++ b/src/services/votes.spec.ts @@ -1,11 +1,11 @@ import * as db from '../db'; import { createDbClient } from '../utils/db/create-db-connection'; import { runMigrations } from '../utils/db/run-migrations'; -import { environmentVariables, insertVotesSchema } from '../types'; +import { environmentVariables } from '../types'; import { cleanup, seed } from '../utils/db/seed'; -import { z } from 'zod'; import { saveVote, + validateVote, queryVoteData, queryGroupCategories, numOfVotesDictionary, @@ -14,7 +14,9 @@ import { calculateQuadraticScore, updateVoteScoreInDatabase, updateVoteScorePlural, + updateVoteScoreQuadratic, userCanVote, + updateOptionScore, } from './votes'; import { eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; @@ -23,7 +25,7 @@ import { Client } from 'pg'; describe('service: votes', () => { let dbPool: NodePgDatabase; let dbConnection: Client; - let testData: z.infer; + let testData: { optionId: string; numOfVotes: number }; let cycle: db.Cycle | undefined; let questionOption: db.Option | undefined; let otherQuestionOption: db.Option | undefined; @@ -72,88 +74,176 @@ describe('service: votes', () => { testData = { numOfVotes: 1, optionId: questionOption?.id ?? '', - questionId: forumQuestion?.id ?? '', - userId: user?.id ?? '', }; }); - test('should save vote', async () => { + test('validation should return false if no option id is specified', async () => { + const response = await validateVote(dbPool, { numOfVotes: 1, optionId: '' }, user!.id ?? ''); + expect(response.isValid).toEqual(false); + expect(response.error).toEqual(expect.any(String)); + }); + + test('validation should return false if a non-existing optionid is specified', async () => { + const response = await validateVote( + dbPool, + { numOfVotes: 1, optionId: '00000000-0000-0000-0000-000000000000' }, + user!.id ?? '', + ); + expect(response.isValid).toEqual(false); + expect(response.error).toEqual(expect.any(String)); + }); + + test('validation should return false if the cycle is not open', async () => { + await dbPool.update(db.cycles).set({ status: 'CLOSED' }).where(eq(db.cycles.id, cycle!.id)); + const response = await validateVote( + dbPool, + { numOfVotes: 1, optionId: questionOption?.id ?? '' }, + user!.id ?? '', + ); + expect(response.isValid).toEqual(false); + expect(response.error).toEqual(expect.any(String)); + }); + + test('validation should return false if a user is not approved', async () => { await dbPool.update(db.cycles).set({ status: 'OPEN' }).where(eq(db.cycles.id, cycle!.id)); - // accept user registration + const response = await validateVote( + dbPool, + { numOfVotes: 1, optionId: questionOption?.id ?? '' }, + user!.id ?? '', + ); + expect(response.isValid).toEqual(false); + expect(response.error).toEqual(expect.any(String)); + }); + + test('validation should return true all validation checks pass', async () => { await dbPool.insert(db.registrations).values({ status: 'APPROVED', userId: user!.id ?? '', eventId: cycle!.eventId ?? '', }); - // Call the saveVote function - const { data: response } = await saveVote(dbPool, testData); - // Check if response is defined + const response = await validateVote( + dbPool, + { numOfVotes: 1, optionId: questionOption?.id ?? '' }, + user!.id ?? '', + ); + expect(response.isValid).toEqual(true); + expect(response.error).toEqual(null); + }); + + test('userCanVote returns false if user does not have an approved registration', async () => { + const response = await userCanVote(dbPool, secondUser!.id ?? '', questionOption?.id ?? ''); + expect(response).toEqual(false); + }); + + test('userCanVote returns true if user has an approved registration', async () => { + await dbPool.insert(db.registrations).values({ + status: 'APPROVED', + userId: secondUser!.id ?? '', + eventId: cycle!.eventId ?? '', + }); + const response = await userCanVote(dbPool, secondUser!.id ?? '', questionOption?.id ?? ''); + expect(response).toEqual(true); + }); + + test('userCanVote returns false if no option id gets provided', async () => { + const response = await userCanVote(dbPool, secondUser!.id ?? '', ''); + expect(response).toEqual(false); + }); + + test('should save vote', async () => { + const { data: response } = await saveVote( + dbPool, + testData, + user?.id ?? '', + forumQuestion?.id ?? '', + ); expect(response).toBeDefined(); - // Check property existence and types expect(response).toHaveProperty('id'); expect(response?.id).toEqual(expect.any(String)); expect(response).toHaveProperty('userId'); expect(response?.userId).toEqual(expect.any(String)); - // check timestamps expect(response?.createdAt).toEqual(expect.any(Date)); expect(response?.updatedAt).toEqual(expect.any(Date)); }); - test('should not save vote if cycle is closed', async () => { - // update cycle to closed state - await dbPool.update(db.cycles).set({ status: 'CLOSED' }).where(eq(db.cycles.id, cycle!.id)); - // Call the saveVote function - const { data: response, errors } = await saveVote(dbPool, testData); + test('should not save vote with invalid test data', async () => { + const invalidTestData = { + optionId: '', + numOfVotes: 2, + }; + const response = await saveVote( + dbPool, + invalidTestData, + user?.id ?? '', + forumQuestion?.id ?? '', + ); + expect(response.data).toBeNull(); + expect(response.error).toBeDefined(); + expect(response.error).toEqual(expect.any(String)); + }); - // expect response to be undefined - expect(response).toBeUndefined(); + test('UpdateOptionScore returns an error if not all question ids are the same', async () => { + const mockData = [ + { optionId: 'option1', numOfVotes: 10 }, + { optionId: 'option2', numOfVotes: 5 }, + { optionId: 'option3', numOfVotes: 2 }, + ]; + const questionIds = [forumQuestion?.id ?? '', otherForumQuestion?.id ?? '']; - // expect error message - expect(errors).toBeDefined(); + const response = await updateOptionScore(dbPool, mockData, questionIds); + expect(response.data).toBeNull(); + expect(response.errors).toBeDefined(); + expect(response.errors[0]).toEqual(expect.any(String)); }); - test('should not allow voting on users that are not registered', async () => { - const canVote = await userCanVote(dbPool, secondUser!.id, questionOption!.id); - expect(canVote).toBe(false); - }); + test('UpdateOptionScore returns an error if no question id is found', async () => { + const mockData = [ + { optionId: 'option1', numOfVotes: 10 }, + { optionId: 'option2', numOfVotes: 5 }, + { optionId: 'option3', numOfVotes: 2 }, + ]; + const questionIds = [ + '00000000-0000-0000-0000-000000000000', + '00000000-0000-0000-0000-000000000000', + ]; - test('should not save vote if cycle is upcoming', async () => { - // update cycle to closed state - await dbPool.update(db.cycles).set({ status: 'UPCOMING' }).where(eq(db.cycles.id, cycle!.id)); - // Call the saveVote function - const { data: response, errors } = await saveVote(dbPool, testData); + const response = await updateOptionScore(dbPool, mockData, questionIds); + expect(response.data).toBeNull(); + expect(response.errors).toBeDefined(); + expect(response.errors[0]).toEqual(expect.any(String)); + }); - // expect response to be undefined - expect(response).toBeUndefined(); + test('UpdateOptionScore returns an error if a valid but non existing uuid gets provided', async () => { + const mockData = [ + { optionId: 'option1', numOfVotes: 10 }, + { optionId: 'option2', numOfVotes: 5 }, + { optionId: 'option3', numOfVotes: 2 }, + ]; + const questionIds = ['', '']; - // expect error message - expect(errors).toBeDefined(); + const response = await updateOptionScore(dbPool, mockData, questionIds); + expect(response.data).toBeNull(); + expect(response.errors).toBeDefined(); + expect(response.errors[0]).toEqual(expect.any(String)); }); test('should fetch vote data correctly', async () => { - // open cycle for voting - await dbPool.update(db.cycles).set({ status: 'OPEN' }).where(eq(db.cycles.id, cycle!.id)); - // register second user await dbPool.insert(db.registrations).values({ status: 'APPROVED', userId: secondUser!.id ?? '', eventId: cycle!.eventId ?? '', }); - // save a second user vote - const res = await saveVote(dbPool, { ...testData, userId: secondUser!.id }); - console.log(res); + await saveVote(dbPool, testData, secondUser!.id, forumQuestion?.id ?? ''); const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); expect(voteArray).toBeDefined(); expect(voteArray).toHaveLength(2); - voteArray?.forEach((vote) => { expect(vote).toHaveProperty('userId'); expect(vote).toHaveProperty('numOfVotes'); expect(typeof vote.numOfVotes).toBe('number'); }); - expect(voteArray[0]?.numOfVotes).toBe(1); }); @@ -208,9 +298,9 @@ describe('service: votes', () => { // Get vote data required for groups const groupCategoriesIdArray = await queryGroupCategories(dbPool, forumQuestion!.id); expect(groupCategoriesIdArray).toBeDefined(); - expect(groupCategoriesIdArray.length).toBe(1); - expect(Array.isArray(groupCategoriesIdArray)).toBe(true); - groupCategoriesIdArray.forEach((categoryId) => { + expect(groupCategoriesIdArray.data!.length).toBe(1); + expect(Array.isArray(groupCategoriesIdArray.data)).toBe(true); + groupCategoriesIdArray.data!.forEach((categoryId) => { expect(typeof categoryId).toBe('string'); }); }); @@ -218,9 +308,7 @@ describe('service: votes', () => { 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(0); - expect(Array.isArray(groupCategoriesIdArray)).toBe(true); - expect(groupCategoriesIdArray).toEqual([]); + expect(groupCategoriesIdArray.data!).toBe(null); }); test('only return groups for users who voted for the option', async () => { @@ -332,14 +420,24 @@ describe('service: votes', () => { expect(updatedDbScore?.voteScore).toBe('100'); }); - test('full integration test of the update vote functionality', async () => { - // Test that the plurality score is correct if both users are in the same group - const score = await updateVoteScorePlural(dbPool, questionOption?.id ?? ''); + test('that the plurality score is correct if both users are in the same group', async () => { + const score = await updateVoteScorePlural( + dbPool, + questionOption?.id ?? '', + forumQuestion?.id ?? '', + ); // sqrt of 2 because the two users are in the same group // voting for the same option with 1 vote each expect(score).toBe(Math.sqrt(2)); }); + test('that the quadratic score is correctly calculated as the sum of square roots', async () => { + const score = await updateVoteScoreQuadratic(dbPool, questionOption?.id ?? ''); + // two users voting for the same option with 1 vote each + // sqrt of 1 + sqrt of 1 = 2 + expect(score).toBe(2); + }); + afterAll(async () => { await cleanup(dbPool); await dbConnection.end(); diff --git a/src/services/votes.ts b/src/services/votes.ts index e6e6bed1..08f3c797 100644 --- a/src/services/votes.ts +++ b/src/services/votes.ts @@ -9,72 +9,142 @@ import { quadraticVoting } from '../modules/quadratic-voting'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; /** - * Saves votes submitted by a user. + * Validates and saves votes submitted by a user. * - * This function validates and saves each vote provided in the `data` array for the specified `userId`. - * It then updates the vote count for each option based on the vote model associated with the question. + * This function validates each vote provided in the `data` array for the specified `userId`. + * If all votes are valid, it saves each vote to the database. * - * @param dbPool - The database connection pool. - * @param data - An array of objects containing `optionId` and `numOfVotes` properties. - * @param userId - The ID of the user submitting the votes. - * @returns A promise that resolves to an object containing `data` (an array of saved votes) and `errors` (an array of error messages). + * @param {NodePgDatabase} dbPool + * @param {{ optionId: string; numOfVotes: number }[]} data + * @param {string} userId + * @returns {Promise<{ data: db.Vote[] | null; errors: string[] }>} */ -export async function saveVotes( +export async function validateAndSaveVotes( dbPool: NodePgDatabase, data: { optionId: string; numOfVotes: number }[], userId: string, -): Promise<{ data: db.Vote[]; errors: string[] }> { +): Promise<{ data: db.Vote[] | null; questionIds: string[]; errors: string[] }> { const voteData: db.Vote[] = []; + const questionIds: string[] = []; const errors: string[] = []; for (const vote of data) { - const { data, error } = await validateAndSaveVote(dbPool, vote, userId); - if (data) { - voteData.push(data); - } - if (error) { + // Validate the vote + const { isValid, error } = await validateVote(dbPool, vote, userId); + if (!isValid && error) { errors.push(error); + continue; + } + + // Find the question option if the vote is valid + const queryQuestionOption = await dbPool.query.options.findFirst({ + where: eq(db.options.id, vote.optionId), + }); + + if (!queryQuestionOption) { + errors.push(`No option found for optionId: ${vote.optionId}`); + continue; + } + + // Save the vote + const { data: savedVote, error: saveError } = await saveVote( + dbPool, + vote, + userId, + queryQuestionOption.questionId, + ); + if (saveError) { + errors.push(saveError); + } else if (savedVote) { + voteData.push(savedVote); + questionIds.push(queryQuestionOption.questionId); } } - const queryQuestionOption = await dbPool.query.options.findFirst({ - where: eq(db.options.id, voteData[0]!.optionId), - }); + return { data: voteData.length > 0 ? voteData : null, questionIds, errors }; +} - if (!queryQuestionOption) { - errors.push('No option found for the provided optionId'); - return { data: voteData, errors }; +/** + * Updates option scores based on the vote model. + * + * @param {NodePgDatabase} dbPool + * @param {{ optionId: string; numOfVotes: number }[]} data + * @param {string[]} questionIds + * @returns {Promise<{ data: { optionId: string; score: number }[] | null; errors: string[] }>} + */ +export async function updateOptionScore( + dbPool: NodePgDatabase, + data: { optionId: string; numOfVotes: number }[], + questionIds: string[], +): Promise<{ data: { optionId: string; score: number }[] | null; errors: string[] }> { + const scores: { optionId: string; score: number }[] = []; + const errors: string[] = []; + + // Check if all questionIds are the same + const firstQuestionId = questionIds[0]; + if (!questionIds.every((questionId) => questionId === firstQuestionId)) { + errors.push('Not all questionIds are the same'); + return { data: null, errors }; } - const queryForumQuestion = await dbPool.query.questions.findFirst({ - where: eq(db.questions.id, queryQuestionOption!.questionId), - }); + if (!firstQuestionId) { + errors.push('No question Id found'); + return { data: null, errors }; + } - if (!queryForumQuestion) { + // Query group data, grouping dimensions, and calculate the score + const queryQuestion = await dbPool + .select({ + questionId: db.questions.id, + voteModel: db.questions.voteModel, + }) + .from(db.questions) + .where(eq(db.questions.id, firstQuestionId)); + + if (queryQuestion.length === 0) { errors.push('No question found for the provided questionId'); - return { data: voteData, errors }; + return { data: null, errors }; + } + + const voteModel = queryQuestion[0]?.voteModel; + + interface VoteModelUpdateFunction { + ({ + dbPool, + optionId, + questionId, + }: { + dbPool: NodePgDatabase; + optionId: string; + questionId: string; + }): Promise; } - // Define available voting models - const voteModelUpdateFunctions = { - COCM: updateVoteScorePlural, - QV: updateVoteScoreQuadratic, + const voteModelUpdateFunctions: Record = { + COCM: ({ dbPool, optionId, questionId }) => updateVoteScorePlural(dbPool, optionId, questionId), + QV: ({ dbPool, optionId }) => updateVoteScoreQuadratic(dbPool, optionId), }; const updateFunction = - voteModelUpdateFunctions[ - queryForumQuestion?.voteModel as keyof typeof voteModelUpdateFunctions - ]; - - const uniqueOptionIds = voteData.map((vote) => vote.optionId); + voteModelUpdateFunctions[voteModel as keyof typeof voteModelUpdateFunctions]; if (!updateFunction) { - errors.push('Unsupported vote model: ' + queryForumQuestion.voteModel); - } else { - await Promise.all(uniqueOptionIds.map((optionId) => updateFunction(dbPool, optionId))); + errors.push('Unsupported vote model: ' + voteModel); + return { data: null, errors }; } - return { data: voteData, errors }; + await Promise.all( + data.map(async ({ optionId }) => { + try { + const score = await updateFunction({ dbPool, optionId, questionId: firstQuestionId }); + scores.push({ optionId: optionId, score: score }); + } catch (error) { + errors.push(`Failed to update score for optionId: ${optionId}`); + } + }), + ); + + return { data: scores.length > 0 ? scores : null, errors }; } /** @@ -116,10 +186,19 @@ export function numOfVotesDictionary(voteArray: Array<{ userId: string; numOfVot return numOfVotesDictionary; } +/** + * Queries the group categories associated with a given question ID from the database. + * + * @param dbPool - The database pool to use for querying. + * @param questionId - The ID of the question to retrieve group categories for. + * @returns A promise that resolves to an object containing: + * - `data`: An array of group category IDs if found, otherwise `null`. + * - `error`: A string describing the error if no group categories are found, otherwise `null`. + */ export async function queryGroupCategories( dbPool: NodePgDatabase, questionId: string, -): Promise { +): Promise<{ data: string[] | null; error: string | null }> { const groupCategories = await dbPool .select({ groupCategoryId: db.questionsToGroupCategories.groupCategoryId, @@ -127,15 +206,13 @@ export async function queryGroupCategories( .from(db.questionsToGroupCategories) .where(eq(db.questionsToGroupCategories.questionId, questionId)); - // Need to due this adjustment because currently groupCategoryId is nullable in the datatable definition. - const groupCategoryIds: string[] = groupCategories.map((category) => category.groupCategoryId!); - - if (groupCategoryIds.length === 0) { - console.error('Group Category ID is Missing'); - return []; + if (groupCategories.length === 0) { + return { data: null, error: 'No group categories found for the given question Id' }; } - return groupCategoryIds; + const groupCategoryIds: string[] = groupCategories.map((category) => category.groupCategoryId); + + return { data: groupCategoryIds, error: null }; } /** @@ -205,7 +282,6 @@ export async function updateVoteScoreInDatabase( optionId: string, score: number, ) { - // Update vote score in the database await dbPool .update(db.options) .set({ @@ -228,21 +304,13 @@ export async function updateVoteScoreInDatabase( export async function updateVoteScorePlural( dbPool: NodePgDatabase, optionId: string, + questionId: string, ): Promise { // Query and transform vote data const voteArray = await queryVoteData(dbPool, optionId); const votesDictionary = await numOfVotesDictionary(voteArray); - - // Query group data, grouping dimensions, and calculate the score - const queryQuestionId = await dbPool - .select({ - questionId: db.options.questionId, - }) - .from(db.options) - .where(eq(db.options.id, optionId)); - - const groupCategories = await queryGroupCategories(dbPool, queryQuestionId[0]!.questionId); - const groupArray = await groupsDictionary(dbPool, votesDictionary, groupCategories ?? []); + const groupCategories = await queryGroupCategories(dbPool, questionId); + const groupArray = await groupsDictionary(dbPool, votesDictionary, groupCategories.data!); const score = await calculatePluralScore(groupArray, votesDictionary); await updateVoteScoreInDatabase(dbPool, optionId, score); @@ -274,101 +342,101 @@ export async function updateVoteScoreQuadratic( } /** - * Validates and saves a vote for a user in the database. + * This function performs several validation steps for the provided vote object: + * 1. Checks if the option ID is provided. + * 2. Verifies the existence of the option in the database. + * 3. Confirms that the associated voting cycle is open. + * 4. Checks if the user is eligible to vote. * - * This function validates the provided vote object, checks if the option exists, - * inserts the vote into the database, and returns the saved vote data or an error message. + * If all validations pass, the vote is considered valid. Otherwise, an appropriate error message is returned. * - * @param { NodePgDatabase} dbPool - The database connection pool. - * @param {{ optionId: string; numOfVotes: number }} vote - The vote object containing option ID and number of votes. - * @param {string} userId - The ID of the user who is voting. + * @param {NodePgDatabase} dbPool + * @param {{ optionId: string; numOfVotes: number }} vote + * @param {string} userId + * @returns {Promise<{ isValid: boolean; error: string | null }>} */ -async function validateAndSaveVote( +export async function validateVote( dbPool: NodePgDatabase, vote: { optionId: string; numOfVotes: number }, userId: string, -): Promise<{ data: db.Vote | null | undefined; error: string | null | undefined }> { +): Promise<{ isValid: boolean; error: string | null }> { if (!vote.optionId) { - return { data: null, error: 'optionId is required' }; + return { isValid: false, error: 'Option Id is required' }; } + // check if the option exists const queryQuestionOption = await dbPool.query.options.findFirst({ where: eq(db.options.id, vote.optionId), }); if (!queryQuestionOption) { - return { data: null, error: 'Option not found' }; + return { isValid: false, error: 'Option not found' }; } - const insertVoteBody: z.infer = { - optionId: vote.optionId, - numOfVotes: vote.numOfVotes, - userId: userId, - questionId: queryQuestionOption.questionId, - }; - - const body = insertVotesSchema.safeParse(insertVoteBody); + // check cycle status + const queryQuestion = await dbPool.query.questions.findFirst({ + where: eq(db.questions.id, queryQuestionOption.questionId), + with: { + cycle: true, + }, + }); - if (!body.success) { - return { data: null, error: body.error.errors[0]?.message }; + if ((queryQuestion?.cycle?.status as CycleStatusType) !== 'OPEN') { + return { isValid: false, error: 'Cycle is not open' }; } - // check if user can vote + // check if the user can vote const canVote = await userCanVote(dbPool, userId, vote.optionId); if (!canVote) { - return { data: null, error: 'User cannot vote' }; - } - - const newVote = await saveVote(dbPool, insertVoteBody); - - if (newVote.errors) { - return { data: null, error: newVote.errors[0]?.message }; - } - - if (!newVote.data) { - return { data: null, error: 'Failed to insert vote' }; + return { isValid: false, error: 'User cannot vote' }; } - return { data: newVote.data, error: null }; + return { isValid: true, error: null }; } /** * Saves a vote in the database. * - * This function checks if the cycle for the given question is open, - * then inserts the provided vote data into the database and returns the saved vote data. - * - * @param { NodePgDatabase} dbPool - The database connection pool. - * @param {z.infer} vote - The vote data to be saved. + * @param { NodePgDatabase} dbPool + * @param {z.infer} vote + * @param {string} userId + * @param {string} questionId */ export async function saveVote( dbPool: NodePgDatabase, - vote: z.infer, -) { - // check if cycle is open - const queryQuestion = await dbPool.query.questions.findFirst({ - where: eq(db.questions.id, vote?.questionId ?? ''), - with: { - cycle: true, - }, - }); + vote: { optionId: string; numOfVotes: number }, + userId: string, + questionId: string, +): Promise<{ data: db.Vote | null; error: string | null | undefined }> { + const insertVoteBody: z.infer = { + optionId: vote.optionId, + numOfVotes: vote.numOfVotes, + userId: userId, + questionId: questionId, + }; - if ((queryQuestion?.cycle?.status as CycleStatusType) !== 'OPEN') { - return { errors: [{ message: 'Cycle is not open' }] }; + const body = insertVotesSchema.safeParse(insertVoteBody); + + if (!body.success) { + return { data: null, error: body.error.errors[0]?.message }; } // save the votes const newVote = await dbPool .insert(votes) .values({ - userId: vote.userId, - numOfVotes: vote.numOfVotes, - optionId: vote.optionId, - questionId: vote.questionId, + userId: insertVoteBody.userId, + numOfVotes: insertVoteBody.numOfVotes, + optionId: insertVoteBody.optionId, + questionId: insertVoteBody.questionId, }) .returning(); - return { data: newVote[0] }; + if (!newVote || newVote.length === 0 || !newVote[0]) { + return { data: null, error: 'Failed to insert vote in the db' }; + } + + return { data: newVote[0], error: null }; } /** From 9ecdeffb093c77e9b94bf3caa52e3251344cee78 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Wed, 10 Jul 2024 14:01:45 +0100 Subject: [PATCH 05/14] Add api routes for options (#443) * initial event registration changes * get validation working on event registrations * fix handler validation * add new fields to options and questions * add linking group to option * fix tests * create api endpoints for users to create options * add userCanCreate to questions table * add api endpoints for options * fix tests * fix migrations * fix drizzle journal * remove unused dependency * fix wrong registration name in wrong service --- ...orful_psynapse.sql => 0029_quick_ares.sql} | 1 + migrations/meta/0029_snapshot.json | 9 +- migrations/meta/_journal.json | 4 +- src/db/questions.ts | 1 + src/handlers/options.ts | 114 +++++++++++++ src/handlers/questions.ts | 2 +- src/handlers/registrations.ts | 30 ++-- src/routers/options.ts | 4 + src/services/groups.spec.ts | 66 +++++++ src/services/groups.ts | 28 ++- src/services/options.ts | 161 ++++++++++++++++++ ...um-questions.spec.ts => questions.spec.ts} | 2 +- .../{forum-questions.ts => questions.ts} | 0 src/services/registrations.spec.ts | 144 ---------------- src/services/registrations.ts | 98 +++++------ 15 files changed, 444 insertions(+), 220 deletions(-) rename migrations/{0029_colorful_psynapse.sql => 0029_quick_ares.sql} (75%) create mode 100644 src/services/options.ts rename src/services/{forum-questions.spec.ts => questions.spec.ts} (97%) rename src/services/{forum-questions.ts => questions.ts} (100%) delete mode 100644 src/services/registrations.spec.ts diff --git a/migrations/0029_colorful_psynapse.sql b/migrations/0029_quick_ares.sql similarity index 75% rename from migrations/0029_colorful_psynapse.sql rename to migrations/0029_quick_ares.sql index c5d35b7d..e81272c3 100644 --- a/migrations/0029_colorful_psynapse.sql +++ b/migrations/0029_quick_ares.sql @@ -1,3 +1,4 @@ +ALTER TABLE "questions" ADD COLUMN "user_can_create" boolean DEFAULT false;--> statement-breakpoint ALTER TABLE "options" ADD COLUMN "group_id" uuid;--> statement-breakpoint DO $$ BEGIN ALTER TABLE "options" ADD CONSTRAINT "options_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action; diff --git a/migrations/meta/0029_snapshot.json b/migrations/meta/0029_snapshot.json index dca61817..250a4c47 100644 --- a/migrations/meta/0029_snapshot.json +++ b/migrations/meta/0029_snapshot.json @@ -1,5 +1,5 @@ { - "id": "ce83aad2-50c3-4f21-9fd5-0d684d95e87c", + "id": "f83795a1-f552-488f-b670-bd7c417424b8", "prevId": "3b3f3fe7-aab6-4ea0-88cd-e1c81a678986", "version": "6", "dialect": "postgresql", @@ -761,6 +761,13 @@ "notNull": true, "default": "'[]'::jsonb" }, + "user_can_create": { + "name": "user_can_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, "created_at": { "name": "created_at", "type": "timestamp", diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 3081ce70..1ab4fe2a 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -208,8 +208,8 @@ { "idx": 29, "version": "6", - "when": 1720190747562, - "tag": "0029_colorful_psynapse", + "when": 1720519200977, + "tag": "0029_quick_ares", "breakpoints": true }, { diff --git a/src/db/questions.ts b/src/db/questions.ts index bb48ad14..92a5f1a5 100644 --- a/src/db/questions.ts +++ b/src/db/questions.ts @@ -14,6 +14,7 @@ export const questions = pgTable('questions', { voteModel: varchar('vote_model', { length: 256 }).notNull().default('COCM'), showScore: boolean('show_score').default(false), fields: jsonb('fields').notNull().default([]), + userCanCreate: boolean('user_can_create').default(false), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); diff --git a/src/handlers/options.ts b/src/handlers/options.ts index b4faadf8..2c1d528d 100644 --- a/src/handlers/options.ts +++ b/src/handlers/options.ts @@ -3,6 +3,15 @@ import type { Request, Response } from 'express'; import * as db from '../db'; import { getOptionUsers, getOptionComments } from '../services/comments'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { insertOptionsSchema } from '../types'; +import { isUserIsPartOfGroup } from '../services/groups'; +import { + getUserOption, + saveOption, + updateOption, + canUserCreateOption, + validateOptionData, +} from '../services/options'; export function getOptionHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { @@ -79,3 +88,108 @@ export function getOptionUsersHandler(dbPool: NodePgDatabase) { } }; } + +export function saveOptionHandler(dbPool: NodePgDatabase) { + return async function (req: Request, res: Response) { + const userId = req.session.userId; + const body = insertOptionsSchema.safeParse(req.body); + + if (!body.success) { + return res.status(400).json({ errors: body.error.issues }); + } + + const brokenRules = await validateOptionData({ + dbPool, + option: body.data, + }); + + if (brokenRules.length > 0) { + return res.status(400).json({ errors: brokenRules }); + } + + const userCanCreate = await canUserCreateOption({ + dbPool, + option: body.data, + }); + + if (!userCanCreate) { + return res.status(401).json({ errors: ['User can not create this option'] }); + } + + const userIsPartOfGroup = await isUserIsPartOfGroup({ + dbPool, + userId, + groupId: body.data.groupId, + }); + + if (!userIsPartOfGroup) { + return res.status(400).json({ errors: ['Can not register for this group'] }); + } + + try { + const out = await saveOption(dbPool, body.data); + return res.json({ data: out }); + } catch (e) { + console.log('error saving option ' + e); + return res.sendStatus(500); + } + }; +} + +export function updateOptionHandler(dbPool: NodePgDatabase) { + return async function (req: Request, res: Response) { + const optionId = req.params.optionId; + + if (!optionId) { + return res.status(400).json({ errors: ['optionId is required'] }); + } + + const userId = req.session.userId; + const body = insertOptionsSchema.safeParse(req.body); + + if (!body.success) { + return res.status(400).json({ errors: body.error.issues }); + } + + const brokenRules = await validateOptionData({ + dbPool, + option: body.data, + }); + + if (brokenRules.length > 0) { + return res.status(400).json({ errors: brokenRules }); + } + + const userIsPartOfGroup = await isUserIsPartOfGroup({ + dbPool, + userId, + groupId: body.data.groupId, + }); + + if (!userIsPartOfGroup) { + return res.status(400).json({ errors: ['Can not register for this group'] }); + } + + const existingOption = await getUserOption({ + dbPool, + optionId, + userId, + }); + + if (!existingOption) { + return res.status(400).json({ errors: ['Cannot update this option'] }); + } + + try { + const out = await updateOption({ + data: body.data, + option: existingOption, + dbPool, + }); + return res.json({ data: out }); + } catch (e) { + console.log('error saving option ' + e); + return res.sendStatus(500); + } + }; +} diff --git a/src/handlers/questions.ts b/src/handlers/questions.ts index c33376a0..bd735f9a 100644 --- a/src/handlers/questions.ts +++ b/src/handlers/questions.ts @@ -1,6 +1,6 @@ import type { Request, Response } from 'express'; import * as db from '../db'; -import { getQuestionHearts } from '../services/forum-questions'; +import { getQuestionHearts } from '../services/questions'; import { executeResultQueries } from '../services/statistics'; import { calculateFunding } from '../services/funding-mechanism'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; diff --git a/src/handlers/registrations.ts b/src/handlers/registrations.ts index 4768bdf4..34334642 100644 --- a/src/handlers/registrations.ts +++ b/src/handlers/registrations.ts @@ -4,10 +4,10 @@ import { insertRegistrationSchema } from '../types'; import { saveRegistration, updateRegistration, - validateUpdateRegistrationAuthorization, - validateCreateRegistrationAuthorization, + getUserRegistration, validateEventFields, } from '../services/registrations'; +import { isUserIsPartOfGroup } from '../services/groups'; import { eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; @@ -59,14 +59,14 @@ export function saveRegistrationHandler(dbPool: NodePgDatabase) { return res.status(400).json({ errors: brokenRules }); } - const canRegisterGroup = await validateCreateRegistrationAuthorization({ + const userIsPartOfGroup = await isUserIsPartOfGroup({ dbPool, userId, groupId: body.data.groupId, }); - if (!canRegisterGroup) { - return res.status(400).json({ errors: ['Cannot register for this group'] }); + if (!userIsPartOfGroup) { + return res.status(400).json({ errors: ['Can not register for this group'] }); } try { @@ -104,23 +104,31 @@ export function updateRegistrationHandler(dbPool: NodePgDatabase) { return res.status(400).json({ errors: brokenRules }); } - const canUpdateRegistration = await validateUpdateRegistrationAuthorization({ + const userIsPartOfGroup = await isUserIsPartOfGroup({ dbPool, - registrationId, userId, groupId: body.data.groupId, }); - if (!canUpdateRegistration) { - return res.status(400).json({ errors: ['Cannot update this registration'] }); + if (!userIsPartOfGroup) { + return res.status(400).json({ errors: ['Can not register for this group'] }); + } + + const existingRegistration = await getUserRegistration({ + dbPool, + registrationId, + userId, + }); + + if (!existingRegistration) { + return res.status(400).json({ errors: ['Can not update this registration'] }); } try { const out = await updateRegistration({ data: body.data, + registration: existingRegistration, dbPool, - registrationId, - userId, }); return res.json({ data: out }); } catch (e) { diff --git a/src/routers/options.ts b/src/routers/options.ts index cddb8cd2..393a9d88 100644 --- a/src/routers/options.ts +++ b/src/routers/options.ts @@ -4,6 +4,8 @@ import { getOptionUsersHandler, getOptionCommentsHandler, getOptionHandler, + saveOptionHandler, + updateOptionHandler, } from '../handlers/options'; import { isLoggedIn } from '../middleware/is-logged-in'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; @@ -11,6 +13,8 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); export function optionsRouter({ dbPool }: { dbPool: NodePgDatabase }) { + router.post('/', isLoggedIn(dbPool), saveOptionHandler(dbPool)); + router.put('/:optionId', isLoggedIn(dbPool)), updateOptionHandler(dbPool); router.get('/:optionId', isLoggedIn(dbPool), getOptionHandler(dbPool)); router.get('/:optionId/comments', isLoggedIn(dbPool), getOptionCommentsHandler(dbPool)); router.get('/:optionId/users', isLoggedIn(dbPool), getOptionUsersHandler(dbPool)); diff --git a/src/services/groups.spec.ts b/src/services/groups.spec.ts index 92667b9d..611479a5 100644 --- a/src/services/groups.spec.ts +++ b/src/services/groups.spec.ts @@ -8,6 +8,7 @@ import { getSecretGroup, getGroupMembers, getGroupRegistrations, + isUserIsPartOfGroup, } from './groups'; import { environmentVariables, insertSimpleRegistrationSchema } from '../types'; import { z } from 'zod'; @@ -150,6 +151,71 @@ describe('service: groups', () => { expect(result).toBeDefined(); }); + describe('authorization', function () { + test('when the user is not in the group', async function () { + const rows = await dbPool + .insert(db.groups) + .values({ + groupCategoryId: groupCategory!.id, + name: 'Test Group', + }) + .returning(); + + if (!rows) { + throw new Error('No group found'); + } + + if (!rows[0]) { + throw new Error('No group found'); + } + + const result = await isUserIsPartOfGroup({ + dbPool, + userId: user!.id, + groupId: rows[0].id, + }); + + expect(result).toBe(false); + }); + test('when the user is in the group', async function () { + const rows = await dbPool + .insert(db.groups) + .values({ + groupCategoryId: groupCategory!.id, + name: 'Test Group', + }) + .returning(); + + if (!rows) { + throw new Error('No group found'); + } + + if (!rows[0]) { + throw new Error('No group found'); + } + + const userGroup = await dbPool + .insert(db.usersToGroups) + .values({ + userId: user!.id, + groupId: rows[0].id, + }) + .returning(); + + if (!userGroup) { + throw new Error('No user group found'); + } + + const result = await isUserIsPartOfGroup({ + dbPool, + userId: user!.id, + groupId: rows[0].id, + }); + + expect(result).toBe(true); + }); + }); + afterAll(async () => { await cleanup(dbPool); await dbConnection.end(); diff --git a/src/services/groups.ts b/src/services/groups.ts index 224d7610..0f93de3d 100644 --- a/src/services/groups.ts +++ b/src/services/groups.ts @@ -1,9 +1,9 @@ -import * as db from '../db'; +import { and, eq } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { z } from 'zod'; +import * as db from '../db'; import { insertGroupsSchema } from '../types/groups'; import { wordlist } from '../utils/db/mnemonics'; -import { eq } from 'drizzle-orm'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; export function createSecretGroup( dbPool: NodePgDatabase, @@ -98,3 +98,25 @@ export async function getGroupRegistrations(dbPool: NodePgDatabase, g return response; } + +export async function isUserIsPartOfGroup({ + dbPool, + userId, + groupId, +}: { + dbPool: NodePgDatabase; + userId: string; + groupId?: string | null; +}) { + if (groupId) { + const userGroup = await dbPool.query.usersToGroups.findFirst({ + where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, groupId)), + }); + + if (!userGroup) { + return false; + } + } + + return true; +} diff --git a/src/services/options.ts b/src/services/options.ts new file mode 100644 index 00000000..5301454b --- /dev/null +++ b/src/services/options.ts @@ -0,0 +1,161 @@ +import { and, eq } from 'drizzle-orm'; +import { z } from 'zod'; +import { fieldsSchema, insertOptionsSchema } from '../types'; +import * as db from '../db'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { enforceRules } from './validation'; + +export async function getUserOption({ + dbPool, + optionId, + userId, +}: { + dbPool: NodePgDatabase; + userId: string; + optionId: string; +}): Promise { + const existingOption = await dbPool.query.options.findFirst({ + where: and(eq(db.options.userId, userId), eq(db.options.id, optionId)), + }); + + if (!existingOption) { + return null; + } + + return existingOption; +} + +export async function saveOption( + dbPool: NodePgDatabase, + data: z.infer, +) { + const newOption = await createOptionInDB(dbPool, { + ...data, + }); + + if (!newOption) { + throw new Error('failed to save option'); + } + + return newOption; +} + +export async function updateOption({ + data, + dbPool, + option, +}: { + dbPool: NodePgDatabase; + data: z.infer; + option: db.Option; +}) { + const updatedRegistration = await updateOptionInDB(dbPool, option, data); + + if (!updatedRegistration) { + throw new Error('failed to save option'); + } + + const out = { + ...updatedRegistration, + }; + + return out; +} + +async function createOptionInDB( + dbPool: NodePgDatabase, + body: z.infer, +) { + const rows = await dbPool + .insert(db.options) + .values({ + userId: body.userId, + questionId: body.questionId, + title: body.title, + subTitle: body.subTitle, + groupId: body.groupId, + data: body.data, + }) + .returning(); + return rows[0]; +} + +async function updateOptionInDB( + dbPool: NodePgDatabase, + option: db.Option, + body: z.infer, +) { + const rows = await dbPool + .update(db.options) + .set({ + groupId: body.groupId, + questionId: body.questionId, + data: body.data, + title: body.title, + subTitle: body.subTitle, + updatedAt: new Date(), + }) + .where(and(eq(db.options.id, option.id))) + .returning(); + return rows[0]; +} + +export async function validateOptionData({ + option, + dbPool, +}: { + dbPool: NodePgDatabase; + option: z.infer; +}) { + const rows = await dbPool + .select() + .from(db.questions) + .where(eq(db.questions.id, option.questionId)); + + if (!rows.length) { + return []; + } + + const question = rows[0]; + + if (!question) { + return []; + } + + // get registration fields for the event + const questionFields = fieldsSchema.safeParse(question.fields); + + if (!questionFields.success) { + return []; + } + + return enforceRules({ + data: option.data, + fields: questionFields.data, + }); +} + +export async function canUserCreateOption({ + option, + dbPool, +}: { + dbPool: NodePgDatabase; + option: z.infer; +}): Promise { + const rows = await dbPool + .select() + .from(db.questions) + .where(eq(db.questions.id, option.questionId)); + + if (!rows.length) { + return false; + } + + const question = rows[0]; + + if (!question) { + return false; + } + + return !!question.userCanCreate; +} diff --git a/src/services/forum-questions.spec.ts b/src/services/questions.spec.ts similarity index 97% rename from src/services/forum-questions.spec.ts rename to src/services/questions.spec.ts index c1f3f737..2eef8770 100644 --- a/src/services/forum-questions.spec.ts +++ b/src/services/questions.spec.ts @@ -1,4 +1,4 @@ -import { availableHearts } from './forum-questions'; +import { availableHearts } from './questions'; // Test availableHearts function describe('service: forumQuestions', () => { diff --git a/src/services/forum-questions.ts b/src/services/questions.ts similarity index 100% rename from src/services/forum-questions.ts rename to src/services/questions.ts diff --git a/src/services/registrations.spec.ts b/src/services/registrations.spec.ts deleted file mode 100644 index 1d4f54ea..00000000 --- a/src/services/registrations.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import * as db from '../db'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; -import { - validateCreateRegistrationAuthorization, - validateUpdateRegistrationAuthorization, -} from './registrations'; -import { environmentVariables } from '../types'; -import { cleanup, seed } from '../utils/db/seed'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; -import { eq } from 'drizzle-orm'; - -describe('service: registrations', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; - let userId: string; - let eventId: string; - beforeAll(async () => { - const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - dbPool = initDb.db; - dbConnection = initDb.client; - // seed - const { users, events } = await seed(dbPool); - userId = users?.[0]?.id ?? ''; - eventId = events?.[0]?.id ?? ''; - }); - - describe('validate: create registration authorization', function () { - test('when the user is not in the group', async function () { - const notRealGroupId = userId; - - const result = await validateCreateRegistrationAuthorization({ - dbPool, - userId, - groupId: notRealGroupId, - }); - - expect(result).toBe(false); - }); - test('when the user is in the group', async function () { - const group = await dbPool.query.usersToGroups.findFirst({ - where: eq(db.usersToGroups.userId, userId), - }); - - if (!group) { - throw new Error('No group found'); - } - - const result = await validateCreateRegistrationAuthorization({ - dbPool, - userId, - groupId: group.groupId, - }); - - expect(result).toBe(true); - }); - }); - - describe('validate: update registration authorization', function () { - test('when the user is not in the group', async function () { - const rows = await dbPool - .insert(db.registrations) - .values({ - eventId: eventId, - userId: userId, - }) - .returning(); - - if (!rows) { - throw new Error('No registration found'); - } - - if (!rows[0]) { - throw new Error('No registration found'); - } - - const notRealGroupId = userId; - const result = await validateUpdateRegistrationAuthorization({ - dbPool, - userId, - groupId: notRealGroupId, - registrationId: rows[0].id, - }); - - expect(result).toBe(false); - }); - test('when the user is in the group', async function () { - const rows = await dbPool - .insert(db.registrations) - .values({ - eventId: eventId, - userId: userId, - }) - .returning(); - - if (!rows) { - throw new Error('No registration found'); - } - - if (!rows[0]) { - throw new Error('No registration found'); - } - - const userGroup = await dbPool.query.usersToGroups.findFirst({ - where: eq(db.usersToGroups.userId, userId), - }); - - if (!userGroup) { - throw new Error('No group found'); - } - - const result = await validateUpdateRegistrationAuthorization({ - dbPool, - userId, - groupId: userGroup.groupId, - registrationId: rows[0].id, - }); - - expect(result).toBe(true); - }); - }); - afterAll(async () => { - // Delete registration data - await cleanup(dbPool); - await dbConnection.end(); - }); -}); diff --git a/src/services/registrations.ts b/src/services/registrations.ts index 16527258..3b2265f3 100644 --- a/src/services/registrations.ts +++ b/src/services/registrations.ts @@ -1,66 +1,28 @@ import { and, eq } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { z } from 'zod'; -import { insertRegistrationSchema, fieldsSchema } from '../types'; import * as db from '../db'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { fieldsSchema, insertRegistrationSchema } from '../types'; import { enforceRules } from './validation'; -export async function validateCreateRegistrationAuthorization({ - dbPool, - userId, - groupId, -}: { - dbPool: NodePgDatabase; - userId: string; - groupId?: string | null; -}) { - if (groupId) { - const userGroup = await dbPool.query.usersToGroups.findFirst({ - where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, groupId)), - }); - - if (!userGroup) { - return false; - } - } - - return true; -} - -export async function validateUpdateRegistrationAuthorization({ +export async function getUserRegistration({ dbPool, registrationId, userId, - groupId, }: { dbPool: NodePgDatabase; userId: string; registrationId: string; - groupId?: string | null; -}) { +}): Promise { 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; + return null; } - if (groupId) { - const userGroup = await dbPool.query.usersToGroups.findFirst({ - where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, groupId)), - }); - - if (!userGroup) { - return false; - } - } - - return true; + return existingRegistration; } export async function validateEventFields({ @@ -126,23 +88,13 @@ export async function saveRegistration( export async function updateRegistration({ data, dbPool, - registrationId, - userId, + registration, }: { dbPool: NodePgDatabase; data: z.infer; - registrationId: string; - userId: string; + registration: db.Registration; }) { - 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); + const updatedRegistration = await updateRegistrationInDB(dbPool, registration, data); if (!updatedRegistration) { throw new Error('failed to save registration'); @@ -190,3 +142,35 @@ async function updateRegistrationInDB( .returning(); return updatedRegistration[0]; } + +export async function validateRegistrationData({ + registration, + dbPool, +}: { + dbPool: NodePgDatabase; + registration: z.infer; +}) { + const rows = await dbPool.select().from(db.events).where(eq(db.events.id, registration.eventId)); + + if (!rows.length) { + return []; + } + + const event = rows[0]; + + if (!event) { + return []; + } + + // get fields for the event + const eventFields = fieldsSchema.safeParse(event.fields); + + if (!eventFields.success) { + return []; + } + + return enforceRules({ + data: registration.data, + fields: eventFields.data, + }); +} From b9a48883186a812b5eeede6bd236bd2f49a043b1 Mon Sep 17 00:00:00 2001 From: Martin Benedikt Busch <43137759+MartinBenediktBusch@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:26:11 +0200 Subject: [PATCH 06/14] update test coverage (#445) * add tests for the user service * fix linting * test funding mechanism and refactor service * resolve lint warnings --- src/services/funding-mechanism.spec.ts | 57 ++++++++ src/services/funding-mechanism.ts | 22 ++- src/services/users.spec.ts | 184 ++++++++++++++++++++++--- src/services/users.ts | 4 +- 4 files changed, 238 insertions(+), 29 deletions(-) create mode 100644 src/services/funding-mechanism.spec.ts diff --git a/src/services/funding-mechanism.spec.ts b/src/services/funding-mechanism.spec.ts new file mode 100644 index 00000000..ee8ddf32 --- /dev/null +++ b/src/services/funding-mechanism.spec.ts @@ -0,0 +1,57 @@ +import * as db from '../db'; +import { createDbClient } from '../utils/db/create-db-connection'; +import { runMigrations } from '../utils/db/run-migrations'; +import { environmentVariables } from '../types'; +import { cleanup, seed } from '../utils/db/seed'; +import { calculateFunding } from './funding-mechanism'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { Client } from 'pg'; + +describe('service: funding-mechanism', () => { + let dbPool: NodePgDatabase; + let dbConnection: Client; + let question: db.Question; + + beforeAll(async () => { + const envVariables = environmentVariables.parse(process.env); + const initDb = await createDbClient({ + database: envVariables.DATABASE_NAME, + host: envVariables.DATABASE_HOST, + password: envVariables.DATABASE_PASSWORD, + user: envVariables.DATABASE_USER, + port: envVariables.DATABASE_PORT, + }); + + await runMigrations({ + database: envVariables.DATABASE_NAME, + host: envVariables.DATABASE_HOST, + password: envVariables.DATABASE_PASSWORD, + user: envVariables.DATABASE_USER, + port: envVariables.DATABASE_PORT, + }); + + dbPool = initDb.db; + dbConnection = initDb.client; + const { forumQuestions } = await seed(dbPool); + question = forumQuestions[0]!; + }); + + test('calculateFunding returns and error if the query returns no optionData', async () => { + const response = await calculateFunding(dbPool, '00000000-0000-0000-0000-000000000000'); + expect(response.allocatedFunding).toBeNull(); + expect(response.remainingFunding).toBeNull(); + expect(response.error).toEqual(expect.any(String)); + }); + + test('calculateFunding returns the correct funding amount', async () => { + const response = await calculateFunding(dbPool, question?.id); + expect(response.allocatedFunding).toBeDefined(); + expect(response.remainingFunding).toEqual(100000); + expect(response.error).toBeNull(); + }); + + afterAll(async () => { + await cleanup(dbPool); + await dbConnection.end(); + }); +}); diff --git a/src/services/funding-mechanism.ts b/src/services/funding-mechanism.ts index 405801b0..603e25fb 100644 --- a/src/services/funding-mechanism.ts +++ b/src/services/funding-mechanism.ts @@ -14,7 +14,11 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; export async function calculateFunding( dbPool: NodePgDatabase, forumQuestionId: string, -): Promise<{ allocated_funding: { [key: string]: number }; remaining_funding: number }> { +): Promise<{ + allocatedFunding: { [key: string]: number } | null; + remainingFunding: number | null; + error: string | null; +}> { const getOptionData = await dbPool .select({ id: db.options.id, @@ -24,15 +28,23 @@ export async function calculateFunding( .from(db.options) .where(eq(db.options.questionId, forumQuestionId)); - if (!getOptionData) { - throw new Error('Error in query getOptionData'); + if (getOptionData.length === 0) { + return { + allocatedFunding: null, + remainingFunding: null, + error: 'Error in query getOptionData', + }; } const funding = allocateFunding(100000, 10000, getOptionData); if (!funding) { - throw new Error('Error in allocating funding'); + return { allocatedFunding: null, remainingFunding: null, error: 'Error in allocating funding' }; } - return funding; + return { + allocatedFunding: funding.allocated_funding, + remainingFunding: funding.remaining_funding, + error: null, + }; } diff --git a/src/services/users.spec.ts b/src/services/users.spec.ts index f85fe930..4873942e 100644 --- a/src/services/users.spec.ts +++ b/src/services/users.spec.ts @@ -1,26 +1,166 @@ +import * as db from '../db'; +import { createDbClient } from '../utils/db/create-db-connection'; +import { runMigrations } from '../utils/db/run-migrations'; +import { environmentVariables, insertUserSchema } from '../types'; +import { cleanup, seed } from '../utils/db/seed'; +import { updateUser, upsertUserData, validateUserData } from './users'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { Client } from 'pg'; import { z } from 'zod'; -import { insertUserSchema } from '../types/users'; - -describe('service: users', function () { - describe('schema: insertUserSchema', function () { - it('should remove empty strings from user data', function () { - const user: z.infer = { - email: '', - username: '', - firstName: '', - lastName: '', - telegram: '', - }; - - const transformedUser: { [key: string]: string | null | string[] | object } = - insertUserSchema.parse(user); - - // loop through all keys and check if they are not empty strings - - for (const key of Object.keys(transformedUser)) { - console.log(key); - expect(transformedUser[key]).not.toBe(''); - } + +describe('service: users', () => { + let dbPool: NodePgDatabase; + let dbConnection: Client; + let userData: { + email: string | null; + username: string | null; + firstName: string | null; + lastName: string | null; + telegram: string | null; + }; + let user: db.User; + let secondUser: db.User; + beforeAll(async () => { + const envVariables = environmentVariables.parse(process.env); + const initDb = await createDbClient({ + database: envVariables.DATABASE_NAME, + host: envVariables.DATABASE_HOST, + password: envVariables.DATABASE_PASSWORD, + user: envVariables.DATABASE_USER, + port: envVariables.DATABASE_PORT, + }); + + await runMigrations({ + database: envVariables.DATABASE_NAME, + host: envVariables.DATABASE_HOST, + password: envVariables.DATABASE_PASSWORD, + user: envVariables.DATABASE_USER, + port: envVariables.DATABASE_PORT, }); + + dbPool = initDb.db; + dbConnection = initDb.client; + // seed + const { users } = await seed(dbPool); + user = users[0]!; + secondUser = users[1]!; + }); + + test('should remove empty strings from user data', function () { + const user: z.infer = { + email: '', + username: '', + firstName: '', + lastName: '', + telegram: '', + }; + + const transformedUser: { [key: string]: string | null | string[] | object } = + insertUserSchema.parse(user); + + // Loop through all keys and check if they are not empty strings + for (const key of Object.keys(transformedUser)) { + expect(transformedUser[key]).not.toBe(''); + } + }); + + test('validateUserData returns an error if email already exists', async () => { + userData = { + email: secondUser?.email ?? null, + username: user?.username ?? null, + firstName: user?.firstName ?? null, + lastName: user?.lastName ?? null, + telegram: user?.telegram ?? null, + }; + + const response = await validateUserData(dbPool, user?.id, userData); + expect(response).toBeDefined(); + expect(response).toEqual(expect.arrayContaining([expect.any(String)])); + }); + + test('validateUserData returns an error if username already exists', async () => { + userData = { + email: user?.email ?? null, + username: secondUser?.username ?? null, + firstName: user?.firstName ?? null, + lastName: user?.lastName ?? null, + telegram: user?.telegram ?? null, + }; + + const response = await validateUserData(dbPool, user?.id, userData); + expect(response).toBeDefined(); + expect(response).toEqual(expect.arrayContaining([expect.any(String)])); + }); + + test('validateUserData returns null if validation is successful', async () => { + userData = { + email: user?.email ?? null, + username: user?.username ?? null, + firstName: 'Some Name' ?? null, + lastName: 'Some Other Name' ?? null, + telegram: user?.telegram ?? null, + }; + + const response = await validateUserData(dbPool, user?.id, userData); + expect(response).toBeNull(); + }); + + test('upsertUserData returns updated user data if insertion is successful', async () => { + userData = { + email: user?.email ?? null, + username: user?.username ?? null, + firstName: 'Some Name' ?? null, + lastName: 'Some Other Name' ?? null, + telegram: user?.telegram ?? null, + }; + + const response = await upsertUserData(dbPool, user?.id, userData); + expect(response).toBeDefined(); + expect(Array.isArray(response)).toBe(true); + const updatedUser = response![0]; + expect(updatedUser!.firstName).toBe('Some Name'); + expect(updatedUser!.lastName).toBe('Some Other Name'); + }); + + test('updateUser returns the respective error if validation fails', async () => { + userData = { + email: user?.email ?? null, + username: secondUser?.username ?? null, + firstName: user?.firstName ?? null, + lastName: user?.lastName ?? null, + telegram: user?.telegram ?? null, + }; + const mockData = { + userId: user?.id, + userData: userData, + }; + + const response = await updateUser(dbPool, mockData); + expect(response.errors).toBeDefined(); + expect(response.errors![0]).toEqual(expect.any(String)); + }); + + test('updateUser returns user data if validation and insertion succeeds', async () => { + userData = { + email: user?.email ?? null, + username: user?.username ?? null, + firstName: 'Some Name' ?? null, + lastName: 'Some Other Name' ?? null, + telegram: user?.telegram ?? null, + }; + const mockData = { + userId: user?.id, + userData: userData, + }; + + const response = await updateUser(dbPool, mockData); + expect(response.data).toBeDefined(); + expect(response.data![0]!.firstName).toBe('Some Name'); + expect(response.data![0]!.lastName).toBe('Some Other Name'); + }); + + afterAll(async () => { + await cleanup(dbPool); + await dbConnection.end(); }); }); diff --git a/src/services/users.ts b/src/services/users.ts index 41058eef..8bd0cfc7 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -11,7 +11,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; * @param {UserData} userData - The user data to check. * @returns {Promise | null>} - An array of errors if user data conflicts, otherwise null. */ -async function validateUserData( +export async function validateUserData( dbPool: NodePgDatabase, userId: string, userData: UserData, @@ -51,7 +51,7 @@ async function validateUserData( * @param {string} userId - The ID of the user to update. * @param {UserData} userData - The updated user data. */ -async function upsertUserData( +export async function upsertUserData( dbPool: NodePgDatabase, userId: string, userData: UserData, From fc5e6cdf3757cc590970cf1849711537827c725e Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Wed, 17 Jul 2024 09:47:39 +0100 Subject: [PATCH 07/14] add logger (#446) * add pino * replace console with logger * fix conflicts * update errors * remove unused import --- jest.config.mjs | 1 - package.json | 3 + pnpm-lock.yaml | 221 +++++++++++++++++++++++++++ scripts/db/seed.ts | 7 +- src/handlers/alerts.ts | 3 +- src/handlers/auth.ts | 11 +- src/handlers/comments.ts | 7 +- src/handlers/events.ts | 3 +- src/handlers/groups.ts | 5 +- src/handlers/options.ts | 9 +- src/handlers/questions.ts | 5 +- src/handlers/registrations.ts | 5 +- src/handlers/users-to-groups.ts | 7 +- src/handlers/users.ts | 11 +- src/handlers/votes.ts | 3 +- src/index.ts | 3 +- src/modules/funding-mechanism.ts | 28 ---- src/modules/plural-voting.spec.ts | 3 +- src/modules/quadratic-voting.spec.ts | 5 +- src/modules/quadratic-voting.ts | 14 -- src/routers/api.ts | 12 +- src/services/auth.ts | 3 +- src/services/comments.ts | 5 +- src/services/questions.ts | 7 +- src/services/statistics.ts | 3 +- src/services/users-to-groups.ts | 7 +- src/services/users.ts | 3 +- src/utils/db/create-db-connection.ts | 3 +- src/utils/logger/index.ts | 13 ++ 29 files changed, 317 insertions(+), 93 deletions(-) create mode 100644 src/utils/logger/index.ts diff --git a/jest.config.mjs b/jest.config.mjs index 8e67a342..7c51acb6 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -20,5 +20,4 @@ export default { collectCoverage: false, collectCoverageFrom: ['src/modules/**/*.ts', 'src/services/**/*.ts'], coveragePathIgnorePatterns: ['/src/handlers/'], - silent: true, // surpress console output for passing tests }; diff --git a/package.json b/package.json index 429e9cd8..3270a819 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,9 @@ "drizzle-zod": "^0.5.1", "express": "^4.18.2", "iron-session": "^6.3.1", + "pino": "^9.2.0", + "pino-http": "^10.2.0", + "pino-pretty": "^11.2.1", "zod": "^3.22.4" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5123df3..b95331b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,15 @@ importers: iron-session: specifier: ^6.3.1 version: 6.3.1(express@4.19.2) + pino: + specifier: ^9.2.0 + version: 9.2.0 + pino-http: + specifier: ^10.2.0 + version: 10.2.0 + pino-pretty: + specifier: ^11.2.1 + version: 11.2.1 zod: specifier: ^3.22.4 version: 3.23.8 @@ -950,6 +959,10 @@ packages: '@zk-kit/incremental-merkle-tree@1.1.0': resolution: {integrity: sha512-WnNR/GQse3lX8zOHMU8zwhgX8u3qPoul8w4GjJ0WDHq+VGJimo7EGheRZ/ILeBQabnlzAerdv3vBqYBehBeoKA==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1019,6 +1032,10 @@ packages: assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1184,6 +1201,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -1258,6 +1278,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -1440,6 +1463,9 @@ packages: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1574,6 +1600,14 @@ packages: event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -1593,6 +1627,9 @@ packages: ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1606,6 +1643,13 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -1777,6 +1821,9 @@ packages: heap@0.2.7: resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -2100,6 +2147,10 @@ packages: node-notifier: optional: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-sha256@0.10.1: resolution: {integrity: sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==} @@ -2342,6 +2393,10 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2487,6 +2542,23 @@ packages: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} + pino-abstract-transport@1.2.0: + resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} + + pino-http@10.2.0: + resolution: {integrity: sha512-am03BxnV3Ckx68OkbH0iZs3indsrH78wncQ6w1w51KroIbvJZNImBKX2X1wjdY8lSyaJ0UrX/dnO2DY3cTeCRw==} + + pino-pretty@11.2.1: + resolution: {integrity: sha512-O05NuD9tkRasFRWVaF/uHLOvoRDFD7tb5VMertr78rbsYFjYp48Vg3477EshVAF5eZaEw+OpDl/tu+B0R5o+7g==} + hasBin: true + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.2.0: + resolution: {integrity: sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==} + hasBin: true + pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -2554,6 +2626,13 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -2562,6 +2641,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2586,6 +2668,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -2613,6 +2698,14 @@ packages: resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} engines: {node: '>=4'} + readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + regexp.prototype.flags@1.5.2: resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} engines: {node: '>= 0.4'} @@ -2666,12 +2759,19 @@ packages: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + seedrandom@3.0.5: resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} @@ -2743,6 +2843,9 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + sonic-boom@4.0.1: + resolution: {integrity: sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==} + source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -2803,6 +2906,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2854,6 +2960,9 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + timers-ext@0.1.7: resolution: {integrity: sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==} @@ -4053,6 +4162,10 @@ snapshots: '@zk-kit/incremental-merkle-tree@1.1.0': {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -4126,6 +4239,8 @@ snapshots: assertion-error@1.1.0: {} + atomic-sleep@1.0.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 @@ -4344,6 +4459,8 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + commander@9.5.0: {} concat-map@0.0.1: {} @@ -4431,6 +4548,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + dateformat@4.6.3: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -4524,6 +4643,10 @@ snapshots: encodeurl@1.0.2: {} + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + env-paths@3.0.0: {} error-ex@1.3.2: @@ -4782,6 +4905,10 @@ snapshots: d: 1.0.2 es5-ext: 0.10.64 + event-target-shim@5.0.1: {} + + events@3.3.0: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.3 @@ -4844,6 +4971,8 @@ snapshots: dependencies: type: 2.7.2 + fast-copy@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.2: @@ -4858,6 +4987,10 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-redact@3.5.0: {} + + fast-safe-stringify@2.1.1: {} + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -5044,6 +5177,8 @@ snapshots: heap@0.2.7: {} + help-me@5.0.0: {} + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -5545,6 +5680,8 @@ snapshots: - supports-color - ts-node + joycon@3.1.1: {} + js-sha256@0.10.1: {} js-sha3@0.8.0: {} @@ -5762,6 +5899,8 @@ snapshots: obuf@1.1.2: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -5896,6 +6035,51 @@ snapshots: pify@3.0.0: {} + pino-abstract-transport@1.2.0: + dependencies: + readable-stream: 4.5.2 + split2: 4.2.0 + + pino-http@10.2.0: + dependencies: + get-caller-file: 2.0.5 + pino: 9.2.0 + pino-std-serializers: 7.0.0 + process-warning: 3.0.0 + + pino-pretty@11.2.1: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pump: 3.0.0 + readable-stream: 4.5.2 + secure-json-parse: 2.7.0 + sonic-boom: 4.0.1 + strip-json-comments: 3.1.1 + + pino-std-serializers@7.0.0: {} + + pino@9.2.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pino-std-serializers: 7.0.0 + process-warning: 3.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.3 + sonic-boom: 4.0.1 + thread-stream: 3.1.0 + pirates@4.0.6: {} pkg-dir@4.2.0: @@ -5941,6 +6125,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + process-warning@3.0.0: {} + + process@0.11.10: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -5951,6 +6139,11 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + pump@3.0.0: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -5969,6 +6162,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + range-parser@1.2.1: {} raw-body@2.5.2: @@ -5998,6 +6193,16 @@ snapshots: normalize-package-data: 2.5.0 path-type: 3.0.0 + readable-stream@4.5.2: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + real-require@0.2.0: {} + regexp.prototype.flags@1.5.2: dependencies: call-bind: 1.0.7 @@ -6050,12 +6255,16 @@ snapshots: es-errors: 1.3.0 is-regex: 1.1.4 + safe-stable-stringify@2.4.3: {} + safer-buffer@2.1.2: {} scheduler@0.23.2: dependencies: loose-envify: 1.4.0 + secure-json-parse@2.7.0: {} + seedrandom@3.0.5: {} semver@5.7.2: {} @@ -6138,6 +6347,10 @@ snapshots: slash@3.0.0: {} + sonic-boom@4.0.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -6211,6 +6424,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -6263,6 +6480,10 @@ snapshots: text-table@0.2.0: {} + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + timers-ext@0.1.7: dependencies: es5-ext: 0.10.64 diff --git a/scripts/db/seed.ts b/scripts/db/seed.ts index 8d8645e0..b2b3e929 100644 --- a/scripts/db/seed.ts +++ b/scripts/db/seed.ts @@ -1,6 +1,7 @@ import { environmentVariables } from '../../src/types'; import { createDbClient } from '../../src/utils/db/create-db-connection'; import { cleanup, seed } from '../../src/utils/db/seed'; +import { logger } from '../../src/utils/logger'; async function main() { if (process.argv.includes('--cleanup')) { @@ -15,7 +16,7 @@ async function main() { await cleanup(db); await client.end(); - console.log('Cleaned up database'); + logger.info('Cleaned up database'); } else { const envVariables = environmentVariables.parse(process.env); const { client, db } = await createDbClient({ @@ -27,13 +28,13 @@ async function main() { }); await seed(db); await client.end(); - console.log('Seeded database'); + logger.info('Seeded database'); } } main() .then(() => process.exit(0)) .catch((error) => { - console.error('Error seeding database:', error); + logger.error('Error seeding database:', error); process.exit(1); }); diff --git a/src/handlers/alerts.ts b/src/handlers/alerts.ts index 63728964..aebb890a 100644 --- a/src/handlers/alerts.ts +++ b/src/handlers/alerts.ts @@ -2,6 +2,7 @@ import type { Request, Response } from 'express'; import * as db from '../db'; import { and, eq, gte, lte, or } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; export function getActiveAlerts(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { @@ -15,7 +16,7 @@ export function getActiveAlerts(dbPool: NodePgDatabase) { return res.json({ data: alerts }); } catch (e) { - console.error(`[ERROR] ${JSON.stringify(e)}`); + logger.error(`[ERROR] ${JSON.stringify(e)}`); return res.sendStatus(500); } }; diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts index f69af9db..fd3717a0 100644 --- a/src/handlers/auth.ts +++ b/src/handlers/auth.ts @@ -4,6 +4,7 @@ import * as db from '../db'; import { createOrSignInPCD } from '../services/auth'; import { verifyUserSchema } from '../types'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; export function destroySessionHandler() { return function (req: Request, res: Response) { @@ -18,7 +19,7 @@ export function verifyPCDHandler(dbPool: NodePgDatabase) { const body = verifyUserSchema.safeParse(req.body); if (!body.success) { - console.error(`[ERROR] ${body.error.errors}`); + logger.error(`[ERROR] ${body.error.errors}`); res.status(400).send({ errors: body.error.errors, }); @@ -30,7 +31,7 @@ export function verifyPCDHandler(dbPool: NodePgDatabase) { const isVerified = await SemaphoreSignaturePCDPackage.verify(pcd); if (!isVerified) { - console.error(`[ERROR] ZK ticket PCD is not valid`); + logger.error(`[ERROR] ZK ticket PCD is not valid`); res.status(401).send(); return; } @@ -41,7 +42,7 @@ export function verifyPCDHandler(dbPool: NodePgDatabase) { }; if (pcdUUID.uuid !== body.data.uuid) { - console.error(`[ERROR] UUID does not match`); + logger.error(`[ERROR] UUID does not match`); res.status(401).send(); return; } @@ -56,12 +57,12 @@ export function verifyPCDHandler(dbPool: NodePgDatabase) { await req.session.save(); return res.status(200).send({ data: user }); } catch (e) { - console.error(`[ERROR] ${e}`); + logger.error(`[ERROR] ${e}`); res.status(401).send(); return; } } catch (error: unknown) { - console.error(`[ERROR] unknown error ${error}`); + logger.error(`[ERROR] unknown error ${error}`); return res.sendStatus(500).send(); } }; diff --git a/src/handlers/comments.ts b/src/handlers/comments.ts index ea5e0a9e..44af5112 100644 --- a/src/handlers/comments.ts +++ b/src/handlers/comments.ts @@ -5,6 +5,7 @@ import { deleteCommentLike, saveCommentLike, userCanLike } from '../services/lik import { insertCommentSchema } from '../types'; import { deleteComment, saveComment, userCanComment } from '../services/comments'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; export function getCommentLikesHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { @@ -65,7 +66,7 @@ export function deleteCommentLikeHandler(dbPool: NodePgDatabase) { return res.json({ data: deletedLike.data }); } catch (e) { - console.error(`[ERROR] ${e}`); + logger.error(`[ERROR] ${e}`); return res.status(500).json({ errors: ['Failed to delete like'] }); } }; @@ -95,7 +96,7 @@ export function saveCommentHandler(dbPool: NodePgDatabase) { const out = await saveComment(dbPool, body.data, userId); return res.json({ data: out }); } catch (e) { - console.log('error saving comment ' + e); + logger.error('error saving comment ' + e); return res.sendStatus(500); } }; @@ -125,7 +126,7 @@ export function deleteCommentHandler(dbPool: NodePgDatabase) { return res.json({ data: deletedComment.data }); } catch (error) { - console.error(error); + logger.error(error); return res.status(500).json({ errors: ['Failed to delete comment'] }); } }; diff --git a/src/handlers/events.ts b/src/handlers/events.ts index 85a26122..e3da0571 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -2,6 +2,7 @@ import { and, eq } from 'drizzle-orm'; import type { Request, Response } from 'express'; import * as db from '../db'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; export function getEventCyclesHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { @@ -105,7 +106,7 @@ export function getEventRegistrationsHandler(dbPool: NodePgDatabase) return res.json({ data: out }); } catch (e) { - console.log('error getting registration ' + e); + logger.error('error getting registration ' + e); return res.sendStatus(500); } }; diff --git a/src/handlers/groups.ts b/src/handlers/groups.ts index 4f6f1177..bbc99139 100644 --- a/src/handlers/groups.ts +++ b/src/handlers/groups.ts @@ -5,6 +5,7 @@ import { canCreateGroupInGroupCategory } from '../services/group-categories'; import { createUsersToGroups } from '../services/users-to-groups'; import { createSecretGroup, getGroupMembers, getGroupRegistrations } from '../services/groups'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; export function createGroupHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { @@ -68,7 +69,7 @@ export function getGroupMembersHandler(dbPool: NodePgDatabase) { // Send response return res.status(200).json({ data: responseData }); } catch (error) { - console.error('Error in getGroupMembers:', error); + logger.error('Error in getGroupMembers:', error); return res.status(500).json({ error: 'Internal Server Error' }); } }; @@ -99,7 +100,7 @@ export function getGroupRegistrationsHandler(dbPool: NodePgDatabase) // Send response return res.status(200).json({ data: responseData }); } catch (error) { - console.error('Error in getGroupRegistrationsHandler:', error); + logger.error('Error in getGroupRegistrationsHandler:', error); return res.status(500).json({ error: 'Internal Server Error' }); } }; diff --git a/src/handlers/options.ts b/src/handlers/options.ts index 2c1d528d..b3915cf4 100644 --- a/src/handlers/options.ts +++ b/src/handlers/options.ts @@ -3,6 +3,7 @@ import type { Request, Response } from 'express'; import * as db from '../db'; import { getOptionUsers, getOptionComments } from '../services/comments'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; import { insertOptionsSchema } from '../types'; import { isUserIsPartOfGroup } from '../services/groups'; import { @@ -52,7 +53,7 @@ export function getOptionCommentsHandler(dbPool: NodePgDatabase) { return res.json({ data: commentsWithUserNames }); } catch (error) { - console.error('Error getting comments: ', error); + logger.error('Error getting comments: ', error); return res.sendStatus(500); } }; @@ -83,7 +84,7 @@ export function getOptionUsersHandler(dbPool: NodePgDatabase) { // Send response return res.status(200).json({ data: responseData }); } catch (error) { - console.error('Error in getOptionUsers:', error); + logger.error('Error in getOptionUsers:', error); return res.status(500).json({ error: 'Internal Server Error' }); } }; @@ -130,7 +131,7 @@ export function saveOptionHandler(dbPool: NodePgDatabase) { const out = await saveOption(dbPool, body.data); return res.json({ data: out }); } catch (e) { - console.log('error saving option ' + e); + logger.error('error saving option ' + e); return res.sendStatus(500); } }; @@ -188,7 +189,7 @@ export function updateOptionHandler(dbPool: NodePgDatabase) { }); return res.json({ data: out }); } catch (e) { - console.log('error saving option ' + e); + logger.error('error saving option ' + e); return res.sendStatus(500); } }; diff --git a/src/handlers/questions.ts b/src/handlers/questions.ts index bd735f9a..ac6b5020 100644 --- a/src/handlers/questions.ts +++ b/src/handlers/questions.ts @@ -4,6 +4,7 @@ import { getQuestionHearts } from '../services/questions'; import { executeResultQueries } from '../services/statistics'; import { calculateFunding } from '../services/funding-mechanism'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; export function getQuestionHeartsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { @@ -44,7 +45,7 @@ export function getResultStatisticsHandler(dbPool: NodePgDatabase) { // Send response return res.status(200).json({ data: responseData }); } catch (error) { - console.error('Error in getResultStatistics:', error); + logger.error('Error in getResultStatistics:', error); return res.status(500).json({ error: 'Internal Server Error' }); } }; @@ -77,7 +78,7 @@ export function getCalculateFundingHandler(dbPool: NodePgDatabase) { if (e instanceof Error) { return res.status(400).json({ errors: [e.message] }); } - console.error(e); + logger.error('Error in getCalculateFunding:', e); return res.status(500).json({ errors: ['An error occurred while calculating funding'] }); } }; diff --git a/src/handlers/registrations.ts b/src/handlers/registrations.ts index 34334642..87c9c516 100644 --- a/src/handlers/registrations.ts +++ b/src/handlers/registrations.ts @@ -10,6 +10,7 @@ import { import { isUserIsPartOfGroup } from '../services/groups'; import { eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; export function getRegistrationDataHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { @@ -73,7 +74,7 @@ export function saveRegistrationHandler(dbPool: NodePgDatabase) { const out = await saveRegistration(dbPool, body.data); return res.json({ data: out }); } catch (e) { - console.log('error saving registration ' + e); + logger.error('error saving registration ' + e); return res.sendStatus(500); } }; @@ -132,7 +133,7 @@ export function updateRegistrationHandler(dbPool: NodePgDatabase) { }); return res.json({ data: out }); } catch (e) { - console.log('error saving registration ' + e); + logger.error('error saving registration ' + e); return res.sendStatus(500); } }; diff --git a/src/handlers/users-to-groups.ts b/src/handlers/users-to-groups.ts index ec62a9b1..c1596c1c 100644 --- a/src/handlers/users-to-groups.ts +++ b/src/handlers/users-to-groups.ts @@ -13,6 +13,7 @@ import { } from '../services/users-to-groups'; import { eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; export function joinGroupsHandler(dbPool: NodePgDatabase) { return async (req: Request, res: Response) => { @@ -69,7 +70,7 @@ export function joinGroupsHandler(dbPool: NodePgDatabase) { return res.status(500).json({ errors: ['An error occurred while joining the group'] }); } } catch (e) { - console.error(e); + logger.error('error joining group ' + e); return res.status(500).json({ errors: ['An error occurred while joining the group'] }); } }; @@ -98,7 +99,7 @@ export function updateGroupsHandler(dbPool: NodePgDatabase) { return res.json({ data: userToGroup }); } catch (e) { - console.error(e); + logger.error('error updating group membership ' + e); return res .status(500) .json({ errors: ['An error occurred while updating group membership'] }); @@ -130,7 +131,7 @@ export function leaveGroupsHandler(dbPool: NodePgDatabase) { if (e instanceof Error) { return res.status(400).json({ errors: [e.message] }); } - console.error(e); + logger.error('error leaving group ' + e); return res.status(500).json({ errors: ['An error occurred while leaving the group'] }); } }; diff --git a/src/handlers/users.ts b/src/handlers/users.ts index fe607a39..bc0b2a98 100644 --- a/src/handlers/users.ts +++ b/src/handlers/users.ts @@ -4,6 +4,7 @@ import * as db from '../db'; import { updateUser } from '../services/users'; import { insertUserSchema } from '../types'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; /** * Retrieves user data from the database. @@ -24,7 +25,7 @@ export function getUserHandler(dbPool: NodePgDatabase) { return res.json({ data: user }); } catch (error: unknown) { - console.error(`[ERROR] ${JSON.stringify(error)}`); + logger.error(`[ERROR] ${JSON.stringify(error)}`); return res.sendStatus(500); } }; @@ -74,7 +75,7 @@ export function updateUserHandler(dbPool: NodePgDatabase) { return res.json({ data: user }); } catch (e) { - console.error(`[ERROR] ${JSON.stringify(e)}`); + logger.error(`error updating user ${e}`); return res.sendStatus(500); } }; @@ -106,7 +107,7 @@ export function getUsersToGroupsHandler(dbPool: NodePgDatabase) { return res.json({ data: query }); } catch (e) { - console.log('error getting groups per user ' + JSON.stringify(e)); + logger.error('error getting groups per user ' + JSON.stringify(e)); return res.status(500).json({ error: 'internal server error' }); } }; @@ -139,7 +140,7 @@ export function getUserAttributesHandler(dbPool: NodePgDatabase) { return res.json({ data: userAttributes }); } catch (error: unknown) { - console.error(`[ERROR] ${JSON.stringify(error)}`); + logger.error(`error getting user attributes ${JSON.stringify(error)}`); return res.sendStatus(500); } }; @@ -205,7 +206,7 @@ export function getUserRegistrationsHandler(dbPool: NodePgDatabase) { }); return res.json({ data: out }); } catch (e) { - console.log('error getting user registrations ' + e); + logger.error('error getting user registrations ' + e); return res.sendStatus(500); } }; diff --git a/src/handlers/votes.ts b/src/handlers/votes.ts index 58c81364..41449fef 100644 --- a/src/handlers/votes.ts +++ b/src/handlers/votes.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import * as db from '../db'; import { validateAndSaveVotes, updateOptionScore } from '../services/votes'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; /** * Handler function that saves votes submitted by a user. @@ -43,7 +44,7 @@ export function saveVotesHandler(dbPool: NodePgDatabase) { return res.json({ data: optionScores.data }); } catch (e) { - console.error(`[ERROR] ${e}`); + logger.error(`error saving votes: ${e}`); return res.status(500).json({ errors: e }); } }; diff --git a/src/index.ts b/src/index.ts index 64cd70e0..b1fb7bef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { apiRouter } from './routers/api'; import { environmentVariables } from './types'; import { createDbPool } from './utils/db/create-db-connection'; import { runMigrations } from './utils/db/run-migrations'; +import { logger } from './utils/logger'; const app = express(); async function main() { @@ -25,7 +26,7 @@ async function main() { app.use('/api', apiRouter({ dbPool: db, cookiePassword: envVariables.COOKIE_PASSWORD })); app.listen(!isNaN(Number(envVariables.PORT)) ? envVariables.PORT! : 8080, '0.0.0.0', () => { - console.log(`Listening on :${envVariables.PORT ?? 8080}`); + logger.info(`Listening on :${envVariables.PORT ?? 8080}`); }); } diff --git a/src/modules/funding-mechanism.ts b/src/modules/funding-mechanism.ts index 3a53202d..9d48305d 100644 --- a/src/modules/funding-mechanism.ts +++ b/src/modules/funding-mechanism.ts @@ -66,31 +66,3 @@ export function allocateFunding( remaining_funding: funding, }; } - -// Example usage: -/* -const funding = 15000; -const maxFunding = 10000; - -const getOptionData = [ - { - id: "ID1", - voteScore: "5.5", - fundingRequest: "10000", - }, - { - id: "ID2", - voteScore: "6", - fundingRequest: "8500", - }, - { - id: "ID3", - voteScore: "8", - fundingRequest: "2500", - }, -]; - -const result = allocateFunding(funding, maxFunding, getOptionData); - -console.log(result); -*/ diff --git a/src/modules/plural-voting.spec.ts b/src/modules/plural-voting.spec.ts index 547beba4..d020d438 100644 --- a/src/modules/plural-voting.spec.ts +++ b/src/modules/plural-voting.spec.ts @@ -1,3 +1,4 @@ +import { logger } from '../utils/logger'; import { PluralVoting } from './plural-voting'; // Define instance outside the tests @@ -247,7 +248,7 @@ describe('clusterMatch', () => { test('calculates plurality score according to connection oriented cluster match', () => { const score = pluralVoting.pluralScoreCalculation(); - console.log('Plurality Score:', score); + logger.debug('Plurality Score:', score); expect(true).toBe(true); }); }); diff --git a/src/modules/quadratic-voting.spec.ts b/src/modules/quadratic-voting.spec.ts index 84ebfe6e..a6e72ccd 100644 --- a/src/modules/quadratic-voting.spec.ts +++ b/src/modules/quadratic-voting.spec.ts @@ -1,3 +1,4 @@ +import { logger } from '../utils/logger'; import { quadraticVoting } from './quadratic-voting'; describe('quadraticVoting', () => { @@ -33,8 +34,8 @@ describe('quadraticVoting', () => { }; const [result, sum] = quadraticVoting(votes); - console.log('Quadratic Votes:', result); - console.log('Sum of Quadratic Votes:', sum); + logger.debug('Quadratic Votes:', result); + logger.debug('Sum of Quadratic Votes:', sum); expect(true).toBe(true); }); }); diff --git a/src/modules/quadratic-voting.ts b/src/modules/quadratic-voting.ts index 4909b011..3cb14904 100644 --- a/src/modules/quadratic-voting.ts +++ b/src/modules/quadratic-voting.ts @@ -27,17 +27,3 @@ export function quadraticVoting(votes: Record): [Record = { - "user1": 4, - "user2": 9, - "user3": 16, -}; - -const [result, sum] = quadraticVoting(votes); - -console.log('Quadratic Votes:', result); -console.log('Sum of Quadratic Votes:', sum); -*/ diff --git a/src/routers/api.ts b/src/routers/api.ts index 29ea428a..892842f7 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -16,8 +16,9 @@ import { registrationsRouter } from './registrations'; import { usersToGroupsRouter } from './users-to-groups'; import { groupCategoriesRouter } from './group-categories'; import { alertsRouter } from './alerts'; - -const router = express.Router(); +import { pinoHttp } from 'pino-http'; +import { logger } from '../utils/logger'; +import type { Request } from 'express'; declare module 'iron-session' { interface IronSessionData { @@ -33,6 +34,7 @@ export function apiRouter({ dbPool: NodePgDatabase; cookiePassword: string; }) { + const router = express.Router(); // setup router.use(express.json()); router.use(express.urlencoded({ extended: true })); @@ -48,6 +50,12 @@ export function apiRouter({ }, }), ); + const middlewareLogger = pinoHttp({ + logger: logger, + genReqId: (req: Request) => req.session.userId, + level: process.env.LOG_LEVEL || 'info', + }); + router.use(middlewareLogger); // routes router.use('/auth', authRouter({ dbPool })); router.use('/users', usersRouter({ dbPool })); diff --git a/src/services/auth.ts b/src/services/auth.ts index 8ed45c5f..0ee4f820 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -1,6 +1,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import * as db from '../db'; import { eq } from 'drizzle-orm'; +import { logger } from '../utils/logger'; export async function createOrSignInPCD( dbPool: NodePgDatabase, @@ -35,7 +36,7 @@ export async function createOrSignInPCD( return user[0]; } catch (error: unknown) { // repeated subject_provider unique key - console.error(`[ERROR] ${error}`); + logger.error(`error creating user: ${error}`); throw new Error('User already exists'); } } else { diff --git a/src/services/comments.ts b/src/services/comments.ts index 10813f72..8309b1c0 100644 --- a/src/services/comments.ts +++ b/src/services/comments.ts @@ -3,6 +3,7 @@ import { insertCommentSchema } from '../types'; import { z } from 'zod'; import * as db from '../db'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; /** * Inserts a new comment into the database. @@ -28,7 +29,7 @@ export async function saveComment( .returning(); return newComment[0]; } catch (error) { - console.error('Error in insertComment: ', error); + logger.error('Error in insertComment: ', error); throw new Error('Failed to insert comment'); } } @@ -254,7 +255,7 @@ export async function getOptionUsers( // Return the first row of query result or null if no data found return queryUsers.rows[0] || null; } catch (error) { - console.error('Error in getOptionUsers:', error); + logger.error('Error in getOptionUsers:', error); throw new Error('Error executing database query'); } } diff --git a/src/services/questions.ts b/src/services/questions.ts index 2325e24c..368d072d 100644 --- a/src/services/questions.ts +++ b/src/services/questions.ts @@ -5,6 +5,7 @@ import { z } from 'zod'; import { insertOptionsSchema } from '../types/options'; import { fieldsSchema } from '../types'; import { enforceRules } from './validation'; +import { logger } from '../utils/logger'; export function availableHearts( numProposals: number, @@ -27,7 +28,7 @@ export function availableHearts( } if (numProposals < 2) { - console.error('Number of proposals must be at least 2'); + logger.debug('Number of proposals must be at least 2'); return 0; } @@ -35,7 +36,7 @@ export function availableHearts( const minHearts = baseDenominator + (numProposals - 2) * baseDenominator; if (maxVotes / minHearts !== maxRatio) { - console.error('baseNumerator/baseDenominator does not equal the specified max ratio'); + logger.debug('baseNumerator/baseDenominator does not equal the specified max ratio'); return 0; } @@ -54,7 +55,7 @@ export async function getQuestionHearts( const numOptions = await dbPool.execute<{ countOptions: number }>( sql.raw(` SELECT count("id") AS "countOptions" - FROM question_options + FROM options WHERE question_id = '${forumQuestionId}' `), ); diff --git a/src/services/statistics.ts b/src/services/statistics.ts index e6c6af70..98c5c11c 100644 --- a/src/services/statistics.ts +++ b/src/services/statistics.ts @@ -1,6 +1,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import * as db from '../db'; import { sql } from 'drizzle-orm'; +import { logger } from '../utils/logger'; type ResultData = { numProposals: number; @@ -268,7 +269,7 @@ export async function executeResultQueries( return responseData; } catch (error) { - console.error('Error in executeQueries:', error); + logger.error('Error in executeQueries:', error); throw new Error('Error executing database queries'); } } diff --git a/src/services/users-to-groups.ts b/src/services/users-to-groups.ts index 36f8f3e1..4a868e75 100644 --- a/src/services/users-to-groups.ts +++ b/src/services/users-to-groups.ts @@ -1,6 +1,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import * as db from '../db'; import { eq, and } from 'drizzle-orm'; +import { logger } from '../utils/logger'; export async function createUsersToGroups( dbPool: NodePgDatabase, @@ -12,7 +13,7 @@ export async function createUsersToGroups( }); if (!group) { - console.error('Group not found with ID:', groupId); + logger.error('Group not found with ID:', groupId); throw new Error('Group not found'); } @@ -21,7 +22,7 @@ export async function createUsersToGroups( }); if (existingUserToGroup) { - console.error(userId, 'is already part of group:', groupId); + logger.error(userId, 'is already part of group:', groupId); throw new Error('User is already part of the group'); } @@ -47,7 +48,7 @@ export async function updateUsersToGroups({ }); if (!group) { - console.error('Group not found with ID:', groupId); + logger.error('Group not found with ID:', groupId); throw new Error('Group not found'); } diff --git a/src/services/users.ts b/src/services/users.ts index 8bd0cfc7..f0034cc0 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -3,6 +3,7 @@ import { and, eq, ne, or } from 'drizzle-orm'; import { UserData, insertUserSchema } from '../types/users'; import { z } from 'zod'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { logger } from '../utils/logger'; /** * Checks user data for existing entries in the database. @@ -72,7 +73,7 @@ export async function upsertUserData( return user; } catch (error) { - console.error('Failed to update user data:', error); + logger.error('Failed to update user data:', error); } } diff --git a/src/utils/db/create-db-connection.ts b/src/utils/db/create-db-connection.ts index c5e93bd4..263278f3 100644 --- a/src/utils/db/create-db-connection.ts +++ b/src/utils/db/create-db-connection.ts @@ -1,6 +1,7 @@ import { drizzle } from 'drizzle-orm/node-postgres'; import { Client, Pool } from 'pg'; import * as db from '../../db'; +import { logger } from '../logger'; /** * creates a postgres database pool connection @@ -30,7 +31,7 @@ export function createDbPool({ // the pool will emit an error on behalf of any idle clients // it contains if a backend error or network partition happens pool.on('error', (err) => { - console.error('Unexpected error on idle client', err); + logger.error('Unexpected error on idle client', err); process.exit(-1); }); diff --git a/src/utils/logger/index.ts b/src/utils/logger/index.ts new file mode 100644 index 00000000..07e128a0 --- /dev/null +++ b/src/utils/logger/index.ts @@ -0,0 +1,13 @@ +import pino from 'pino'; + +// this requires pino-pretty to be installed as dev dep +export const logger = pino({ + transport: { + target: process.env.NODE_ENV === 'production' ? 'pino' : 'pino-pretty', + // FATAL, ERROR, WARN, INFO, DEBUG, TRACE + level: process.env.LOG_LEVEL || 'info', + options: { + colorize: true, + }, + }, +}); From 468adcc39342eb21f38c6ed87a3ab477e682e7ac Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Wed, 17 Jul 2024 11:20:16 +0100 Subject: [PATCH 08/14] add group categories to events (#447) --- .github/workflows/check-formatting.yml | 16 +++++++++++----- .github/workflows/deploy-development.yml | 14 +++++++++----- .github/workflows/deploy-production.yml | 17 +++++++++-------- .github/workflows/lint.yml | 15 ++++++++++----- .github/workflows/test.yml | 16 +++++++--------- src/db/events.ts | 2 ++ 6 files changed, 48 insertions(+), 32 deletions(-) diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index 56a739ce..17274f29 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -18,15 +18,21 @@ jobs: name: Review formatting timeout-minutes: 2 runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20] + steps: - - uses: actions/checkout@v4 + - name: Check out repository code + uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 with: - version: 8 - - name: Setup Node.js environment - uses: actions/setup-node@v3 + version: 9 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' + node-version: ${{ matrix.node-version }} cache: 'pnpm' - name: Install dependencies diff --git a/.github/workflows/deploy-development.yml b/.github/workflows/deploy-development.yml index c0d1456e..e5de362c 100644 --- a/.github/workflows/deploy-development.yml +++ b/.github/workflows/deploy-development.yml @@ -19,17 +19,21 @@ env: jobs: build-and-push: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20] + steps: - - name: Checkout code + - name: Check out repository code uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: - version: 8 - - name: Setup Node.js environment - uses: actions/setup-node@v3 + version: 9 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' + node-version: ${{ matrix.node-version }} cache: 'pnpm' - name: Install dependencies diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index f7226f4e..65223a7c 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -19,22 +19,23 @@ env: jobs: build-and-push: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20] + steps: - - name: Checkout code + - name: Check out repository code uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: - version: 8 - - name: Setup Node.js environment - uses: actions/setup-node@v3 + version: 9 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' + node-version: ${{ matrix.node-version }} cache: 'pnpm' - - name: Install dependencies - run: pnpm install - - name: Run Version Bump Script run: scripts/workflows/bump-version.sh "${{ join(github.event.pull_request.labels.*.name, ' ') }}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ff1afcd7..1f802863 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,16 +18,21 @@ jobs: name: Check linting timeout-minutes: 2 runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20] steps: - - uses: actions/checkout@v4 + - name: Check out repository code + uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 with: - version: 8 - - name: Setup Node.js environment - uses: actions/setup-node@v3 + version: 9 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' + node-version: ${{ matrix.node-version }} cache: 'pnpm' - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fdc2e002..b85b391f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,16 +10,14 @@ on: jobs: testing: - # You must use a Linux environment when using service containers or container jobs runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20] - # Service containers to run with `runner-job` services: - # Label used to access the service container postgres: - # Docker Hub image image: postgres - # Provide the password for postgres env: POSTGRES_PASSWORD: secretpassword POSTGRES_USER: postgres @@ -40,11 +38,11 @@ jobs: - uses: pnpm/action-setup@v4 with: - version: 8 - - name: Setup Node.js environment - uses: actions/setup-node@v3 + version: 9 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' + node-version: ${{ matrix.node-version }} cache: 'pnpm' - name: Install dependencies diff --git a/src/db/events.ts b/src/db/events.ts index 4cd45ca2..08076458 100644 --- a/src/db/events.ts +++ b/src/db/events.ts @@ -3,6 +3,7 @@ import { pgTable, timestamp, uuid, varchar, integer, boolean, jsonb } from 'driz import { registrations } from './registrations'; import { cycles } from './cycles'; import { registrationFields } from './registration-fields'; +import { groupCategories } from './group-categories'; export const events = pgTable('events', { id: uuid('id').primaryKey().defaultRandom(), @@ -22,6 +23,7 @@ export const eventsRelations = relations(events, ({ many }) => ({ registrations: many(registrations), registrationFields: many(registrationFields), cycles: many(cycles), + groupCategories: many(groupCategories), })); export type Event = typeof events.$inferSelect; From 3070c5d66083cace5c7711ad0409a1834c9fa089 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Wed, 17 Jul 2024 14:16:11 +0100 Subject: [PATCH 09/14] improve seeding and all cli commands (#448) --- package.json | 22 ++--- pnpm-lock.yaml | 253 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 3270a819..c07f048b 100644 --- a/package.json +++ b/package.json @@ -12,18 +12,17 @@ "dev:node": "node --watch --env-file=.env dist/index.js", "dev:esbuild": "pnpm run build:watch", "dev": "run-p dev:*", - "db:generate": "drizzle-kit generate", - "db:drop": "drizzle-kit drop", - "db:check": "drizzle-kit check", - "db:studio": "drizzle-kit studio", - "db:up": "drizzle-kit up", - "db:seed": "pnpm exec esbuild ./scripts/db/seed.ts --bundle --platform=node --outfile=dist/seed.js && node --env-file=.env dist/seed.js", + "db:generate": "pnpm exec drizzle-kit generate", + "db:drop": "pnpm exec drizzle-kit drop", + "db:check": "pnpm exec drizzle-kit check", + "db:studio": "pnpm exec drizzle-kit studio", + "db:up": "pnpm exec drizzle-kit up", + "db:seed": "node --import tsx --env-file=.env ./scripts/db/seed.ts", "db:seed:cleanup": "pnpm run db:seed --cleanup", - "db:insert:groups": "pnpm exec 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}\"" + "test": "pnpm exec jest --runInBand", + "test:coverage": "pnpm exec jest --coverage --runInBand", + "format": "pnpm exec prettier --check \"src/**/*.{ts,md}\"", + "format:fix": "pnpm exec prettier --write \"src/**/*.{ts,md}\"" }, "keywords": [], "author": "", @@ -51,6 +50,7 @@ "pg": "^8.11.5", "prettier": "^3.1.1", "ts-jest": "^29.1.1", + "tsx": "^4.16.2", "typescript": "^5.5.2" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b95331b0..f9fee2df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,6 +99,9 @@ importers: ts-jest: specifier: ^29.1.1 version: 29.1.2(@babel/core@7.24.5)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.5))(esbuild@0.19.12)(jest@29.7.0(@types/node@20.12.12))(typescript@5.5.2) + tsx: + specifier: ^4.16.2 + version: 4.16.2 typescript: specifier: ^5.5.2 version: 5.5.2 @@ -304,6 +307,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -316,6 +325,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -328,6 +343,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -340,6 +361,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -352,6 +379,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -364,6 +397,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -376,6 +415,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -388,6 +433,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -400,6 +451,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -412,6 +469,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -424,6 +487,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -436,6 +505,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -448,6 +523,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -460,6 +541,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -472,6 +559,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -484,6 +577,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -496,6 +595,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -508,6 +613,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -520,6 +631,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -532,6 +649,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -544,6 +667,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -556,6 +685,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -568,6 +703,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1526,6 +1667,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} @@ -3011,6 +3157,11 @@ packages: tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tsx@4.16.2: + resolution: {integrity: sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3406,138 +3557,207 @@ snapshots: '@esbuild/aix-ppc64@0.19.12': optional: true + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true '@esbuild/android-arm64@0.19.12': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm@0.18.20': optional: true '@esbuild/android-arm@0.19.12': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-x64@0.18.20': optional: true '@esbuild/android-x64@0.19.12': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true '@esbuild/darwin-arm64@0.19.12': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true '@esbuild/darwin-x64@0.19.12': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true '@esbuild/freebsd-arm64@0.19.12': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true '@esbuild/freebsd-x64@0.19.12': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true '@esbuild/linux-arm64@0.19.12': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true '@esbuild/linux-arm@0.19.12': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true '@esbuild/linux-ia32@0.19.12': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true '@esbuild/linux-loong64@0.19.12': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true '@esbuild/linux-mips64el@0.19.12': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true '@esbuild/linux-ppc64@0.19.12': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true '@esbuild/linux-riscv64@0.19.12': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true '@esbuild/linux-s390x@0.19.12': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true '@esbuild/linux-x64@0.19.12': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true '@esbuild/netbsd-x64@0.19.12': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true '@esbuild/openbsd-x64@0.19.12': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true '@esbuild/sunos-x64@0.19.12': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true '@esbuild/win32-arm64@0.19.12': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true '@esbuild/win32-ia32@0.19.12': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true '@esbuild/win32-x64@0.19.12': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 @@ -4807,6 +5027,32 @@ snapshots: '@esbuild/win32-ia32': 0.19.12 '@esbuild/win32-x64': 0.19.12 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + escalade@3.1.2: {} escape-html@1.0.3: {} @@ -6523,6 +6769,13 @@ snapshots: tslib@2.6.2: {} + tsx@4.16.2: + dependencies: + esbuild: 0.21.5 + get-tsconfig: 4.7.5 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 From a193ad8d8795491b0eaf1f47b9bc138676b9f889 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Fri, 19 Jul 2024 16:13:40 +0100 Subject: [PATCH 10/14] Update db interactions and tests (#449) * organize db functions inside db folder * wip: migrate seed data * fix tests imports * update logger for testing * add node test runner * make all tests pass * allow parallel testing * update logger types * update ci test * fix build issues * add test coverage * remove nyc output * remove index from nyc output * fix seed script * exclude spec files from report --------- Co-authored-by: Martin Benedikt Busch --- .github/workflows/test.yml | 3 +- .gitignore | 5 +- .nycrc | 15 + drizzle.config.js | 2 +- jest.config.mjs | 23 - package.json | 9 +- pnpm-lock.yaml | 369 ++++++++++++- scripts/db/seed.ts | 3 +- src/{utils => }/db/create-db-connection.ts | 8 +- src/db/index.ts | 25 +- src/{utils => }/db/run-migrations.ts | 0 src/db/{ => schema}/alerts.ts | 0 src/db/{ => schema}/comments.ts | 0 src/db/{ => schema}/cycles.ts | 0 src/db/{ => schema}/events.ts | 0 src/db/{ => schema}/federated-credentials.ts | 0 src/db/{ => schema}/group-categories.ts | 0 src/db/{ => schema}/groups.ts | 0 src/db/schema/index.ts | 21 + src/db/{ => schema}/likes.ts | 0 src/db/{ => schema}/notification-types.ts | 0 src/db/{ => schema}/options.ts | 0 .../questions-to-group-categories.ts | 0 src/db/{ => schema}/questions.ts | 0 src/db/{ => schema}/registration-data.ts | 0 .../registration-field-options.ts | 0 src/db/{ => schema}/registration-fields.ts | 0 src/db/{ => schema}/registrations.ts | 0 src/db/{ => schema}/user-attributes.ts | 0 src/db/{ => schema}/users-to-groups.ts | 0 src/db/{ => schema}/users-to-notifications.ts | 0 src/db/{ => schema}/users.ts | 0 src/db/{ => schema}/votes.ts | 0 src/db/seed.ts | 515 ++++++++++++++++++ src/db/test.ts | 80 +++ src/handlers/alerts.ts | 8 +- src/handlers/auth.ts | 4 +- src/handlers/comments.ts | 14 +- src/handlers/cycles.ts | 15 +- src/handlers/events.ts | 29 +- src/handlers/group-categories.ts | 14 +- src/handlers/groups.ts | 22 +- src/handlers/options.ts | 26 +- src/handlers/questions.ts | 20 +- src/handlers/registrations.ts | 10 +- src/handlers/users-to-groups.ts | 10 +- src/handlers/users.ts | 36 +- src/handlers/votes.ts | 7 +- src/index.ts | 3 +- src/middleware/is-logged-in.ts | 10 +- src/modules/funding-mechanism.spec.ts | 10 +- src/modules/plural-voting.spec.ts | 42 +- src/modules/quadratic-voting.spec.ts | 13 +- src/routers/alerts.ts | 4 +- src/routers/api.ts | 4 +- src/routers/auth.ts | 4 +- src/routers/comments.ts | 4 +- src/routers/cycles.ts | 4 +- src/routers/events.ts | 4 +- src/routers/group-categories.ts | 4 +- src/routers/groups.ts | 4 +- src/routers/options.ts | 4 +- src/routers/questions.ts | 4 +- src/routers/registrations.ts | 4 +- src/routers/users-to-groups.ts | 4 +- src/routers/users.ts | 4 +- src/routers/votes.ts | 4 +- src/services/auth.ts | 20 +- src/services/comments.spec.ts | 84 ++- src/services/comments.ts | 51 +- src/services/cycles.spec.ts | 85 ++- src/services/cycles.ts | 14 +- src/services/funding-mechanism.spec.ts | 57 +- src/services/funding-mechanism.ts | 14 +- src/services/group-categories.spec.ts | 67 +-- src/services/group-categories.ts | 10 +- src/services/groups.spec.ts | 100 ++-- src/services/groups.ts | 36 +- src/services/likes.ts | 35 +- src/services/options.ts | 38 +- src/services/questions.spec.ts | 14 +- src/services/questions.ts | 23 +- src/services/registrations.ts | 45 +- src/services/statistics.spec.ts | 93 ++-- src/services/statistics.ts | 8 +- src/services/users-to-groups.spec.ts | 95 ++-- src/services/users-to-groups.ts | 48 +- src/services/users.spec.ts | 89 ++- src/services/users.ts | 27 +- src/services/validation.spec.ts | 44 +- src/services/votes.spec.ts | 283 +++++----- src/services/votes.ts | 99 ++-- src/types/comments.ts | 2 +- src/types/groups.ts | 2 +- src/types/options.ts | 2 +- src/types/registrations.ts | 2 +- src/types/users.ts | 2 +- src/types/votes.ts | 2 +- src/utils/db/seed-data-generators.ts | 228 -------- src/utils/db/seed.ts | 440 --------------- src/utils/logger/index.ts | 20 +- src/utils/{db => }/mnemonics.ts | 0 102 files changed, 1826 insertions(+), 1780 deletions(-) create mode 100644 .nycrc delete mode 100644 jest.config.mjs rename src/{utils => }/db/create-db-connection.ts (88%) rename src/{utils => }/db/run-migrations.ts (100%) rename src/db/{ => schema}/alerts.ts (100%) rename src/db/{ => schema}/comments.ts (100%) rename src/db/{ => schema}/cycles.ts (100%) rename src/db/{ => schema}/events.ts (100%) rename src/db/{ => schema}/federated-credentials.ts (100%) rename src/db/{ => schema}/group-categories.ts (100%) rename src/db/{ => schema}/groups.ts (100%) create mode 100644 src/db/schema/index.ts rename src/db/{ => schema}/likes.ts (100%) rename src/db/{ => schema}/notification-types.ts (100%) rename src/db/{ => schema}/options.ts (100%) rename src/db/{ => schema}/questions-to-group-categories.ts (100%) rename src/db/{ => schema}/questions.ts (100%) rename src/db/{ => schema}/registration-data.ts (100%) rename src/db/{ => schema}/registration-field-options.ts (100%) rename src/db/{ => schema}/registration-fields.ts (100%) rename src/db/{ => schema}/registrations.ts (100%) rename src/db/{ => schema}/user-attributes.ts (100%) rename src/db/{ => schema}/users-to-groups.ts (100%) rename src/db/{ => schema}/users-to-notifications.ts (100%) rename src/db/{ => schema}/users.ts (100%) rename src/db/{ => schema}/votes.ts (100%) create mode 100644 src/db/seed.ts create mode 100644 src/db/test.ts delete mode 100644 src/utils/db/seed-data-generators.ts delete mode 100644 src/utils/db/seed.ts rename src/utils/{db => }/mnemonics.ts (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b85b391f..491027fc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,8 @@ jobs: run: pnpm install - name: Run unit tests - run: pnpm test + # creates a .env so node does not throw error + run: touch .env && pnpm test env: DATABASE_PASSWORD: secretpassword DATABASE_USER: postgres diff --git a/.gitignore b/.gitignore index 81e6f501..85e8416d 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,8 @@ yarn-error.log* .env.production.local .direnv -# input files -groups.csv +# testing +.nyc_output + tmp \ No newline at end of file diff --git a/.nycrc b/.nycrc new file mode 100644 index 00000000..3ddf5ba0 --- /dev/null +++ b/.nycrc @@ -0,0 +1,15 @@ +{ + "extends": "@istanbuljs/nyc-config-typescript", + "all": true, + "reporter": [ + "text", + "lcov" + ], + "include": [ + "src/services/*.ts", + "src/modules/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} \ No newline at end of file diff --git a/drizzle.config.js b/drizzle.config.js index b0e3c37f..da8896b1 100644 --- a/drizzle.config.js +++ b/drizzle.config.js @@ -5,7 +5,7 @@ const envVariables = environmentVariables.parse(process.env); /** @type { import("drizzle-kit").Config } */ export default { dialect: 'postgresql', - schema: './src/db/*', + schema: './src/db/schema/*', out: './migrations', dbCredentials: { user: envVariables.DATABASE_USER, diff --git a/jest.config.mjs b/jest.config.mjs deleted file mode 100644 index 7c51acb6..00000000 --- a/jest.config.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import { loadEnvFile } from 'node:process'; - -try { - loadEnvFile(); -} catch { - // do nothing -} - -/** - * @type {import('@jest/types').Config.InitialOptions} - */ -export default { - transform: { - '^.+\\.tsx?$': ['ts-jest', { tsconfig: './tsconfig.json' }], - }, - 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'], - coveragePathIgnorePatterns: ['/src/handlers/'], -}; diff --git a/package.json b/package.json index c07f048b..21bd6b40 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "db:up": "pnpm exec drizzle-kit up", "db:seed": "node --import tsx --env-file=.env ./scripts/db/seed.ts", "db:seed:cleanup": "pnpm run db:seed --cleanup", - "test": "pnpm exec jest --runInBand", - "test:coverage": "pnpm exec jest --coverage --runInBand", + "test": "node --import tsx --env-file=.env --test ./src/**/*.spec.ts", + "test:coverage": "nyc pnpm run test", "format": "pnpm exec prettier --check \"src/**/*.{ts,md}\"", "format:fix": "pnpm exec prettier --write \"src/**/*.{ts,md}\"" }, @@ -31,11 +31,10 @@ "node": "^20.14.0" }, "devDependencies": { - "@jest/types": "^29.6.3", + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@ngneat/falso": "^7.1.1", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", - "@types/jest": "^29.5.11", "@types/node": "^20.12.12", "@types/pg": "^8.11.6", "@typescript-eslint/eslint-plugin": "^6.19.0", @@ -45,8 +44,8 @@ "esbuild": "^0.19.8", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", - "jest": "^29.7.0", "npm-run-all": "^4.1.5", + "nyc": "^17.0.0", "pg": "^8.11.5", "prettier": "^3.1.1", "ts-jest": "^29.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9fee2df..953cbc73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,9 +42,9 @@ importers: specifier: ^3.22.4 version: 3.23.8 devDependencies: - '@jest/types': - specifier: ^29.6.3 - version: 29.6.3 + '@istanbuljs/nyc-config-typescript': + specifier: ^1.0.2 + version: 1.0.2(nyc@17.0.0) '@ngneat/falso': specifier: ^7.1.1 version: 7.2.0 @@ -54,9 +54,6 @@ importers: '@types/express': specifier: ^4.17.21 version: 4.17.21 - '@types/jest': - specifier: ^29.5.11 - version: 29.5.12 '@types/node': specifier: ^20.12.12 version: 20.12.12 @@ -84,12 +81,12 @@ importers: eslint-config-prettier: specifier: ^9.1.0 version: 9.1.0(eslint@8.57.0) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@20.12.12) npm-run-all: specifier: ^4.1.5 version: 4.1.5 + nyc: + specifier: ^17.0.0 + version: 17.0.0 pg: specifier: ^8.11.5 version: 8.11.5 @@ -763,6 +760,12 @@ packages: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} + '@istanbuljs/nyc-config-typescript@1.0.2': + resolution: {integrity: sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==} + engines: {node: '>=8'} + peerDependencies: + nyc: '>=15' + '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -976,9 +979,6 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - '@types/jest@29.5.12': - resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1118,6 +1118,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1145,6 +1149,13 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + append-transform@2.0.0: + resolution: {integrity: sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==} + engines: {node: '>=8'} + + archy@1.0.0: + resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1262,6 +1273,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + caching-transform@4.0.0: + resolution: {integrity: sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==} + engines: {node: '>=8'} + call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} @@ -1314,10 +1329,17 @@ packages: cjs-module-lexer@1.3.1: resolution: {integrity: sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==} + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + cli-color@2.0.4: resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} engines: {node: '>=0.10'} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1349,6 +1371,9 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1360,6 +1385,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1439,6 +1467,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -1458,6 +1490,10 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + default-require-extensions@3.0.1: + resolution: {integrity: sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==} + engines: {node: '>=8'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1642,6 +1678,9 @@ packages: resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} engines: {node: '>=0.10'} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + es6-iterator@2.0.3: resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} @@ -1817,6 +1856,10 @@ packages: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} engines: {node: '>= 0.8'} + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -1835,6 +1878,10 @@ packages: for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + foreground-child@2.0.0: + resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} + engines: {node: '>=8.0.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1843,6 +1890,9 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fromentries@1.3.2: + resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1901,6 +1951,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} @@ -1960,6 +2011,10 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hasha@5.2.2: + resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} + engines: {node: '>=8'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2011,6 +2066,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} @@ -2131,9 +2190,16 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2144,6 +2210,10 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} + istanbul-lib-hook@3.0.0: + resolution: {integrity: sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==} + engines: {node: '>=8'} + istanbul-lib-instrument@5.2.1: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} @@ -2152,6 +2222,10 @@ packages: resolution: {integrity: sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==} engines: {node: '>=10'} + istanbul-lib-processinfo@2.0.3: + resolution: {integrity: sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==} + engines: {node: '>=8'} + istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} @@ -2379,6 +2453,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.flattendeep@4.4.0: + resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -2404,6 +2481,10 @@ packages: lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -2502,6 +2583,10 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-preload@0.2.1: + resolution: {integrity: sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==} + engines: {node: '>=8'} + node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} @@ -2521,6 +2606,11 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + nyc@17.0.0: + resolution: {integrity: sha512-ISp44nqNCaPugLLGGfknzQwSwt10SSS5IMoPR7GLoMAyS18Iw5js8U7ga2VF9lYuMZ42gOHr3UddZw4WZltxKg==} + engines: {node: '>=18'} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2574,10 +2664,18 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@3.0.0: + resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} + engines: {node: '>=8'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-hash@4.0.0: + resolution: {integrity: sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==} + engines: {node: '>=8'} + pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} @@ -2772,6 +2870,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + process-on-spawn@1.0.0: + resolution: {integrity: sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==} + engines: {node: '>=8'} + process-warning@3.0.0: resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} @@ -2856,10 +2958,17 @@ packages: resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} engines: {node: '>= 0.4'} + release-zalgo@1.0.0: + resolution: {integrity: sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==} + engines: {node: '>=4'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -2942,6 +3051,9 @@ packages: resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3002,6 +3114,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + spawn-wrap@2.0.0: + resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==} + engines: {node: '>=8'} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -3178,6 +3294,10 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -3201,6 +3321,9 @@ packages: resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} engines: {node: '>= 0.4'} + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + typescript@5.5.2: resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} engines: {node: '>=14.17'} @@ -3266,6 +3389,9 @@ packages: which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-typed-array@1.1.15: resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} engines: {node: '>= 0.4'} @@ -3286,6 +3412,10 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3293,6 +3423,9 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + write-file-atomic@4.0.2: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -3301,6 +3434,9 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3308,10 +3444,18 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -3833,6 +3977,11 @@ snapshots: js-yaml: 3.14.1 resolve-from: 5.0.0 + '@istanbuljs/nyc-config-typescript@1.0.2(nyc@17.0.0)': + dependencies: + '@istanbuljs/schema': 0.1.3 + nyc: 17.0.0 + '@istanbuljs/schema@0.1.3': {} '@jest/console@29.7.0': @@ -4217,11 +4366,6 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 - '@types/jest@29.5.12': - dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 - '@types/json-schema@7.0.15': {} '@types/keygrip@1.0.6': {} @@ -4397,6 +4541,11 @@ snapshots: acorn@8.11.3: {} + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -4425,6 +4574,12 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + append-transform@2.0.0: + dependencies: + default-require-extensions: 3.0.1 + + archy@1.0.0: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -4596,6 +4751,13 @@ snapshots: bytes@3.1.2: {} + caching-transform@4.0.0: + dependencies: + hasha: 5.2.2 + make-dir: 3.1.0 + package-hash: 4.0.0 + write-file-atomic: 3.0.3 + call-bind@1.0.7: dependencies: es-define-property: 1.0.0 @@ -4649,6 +4811,8 @@ snapshots: cjs-module-lexer@1.3.1: {} + clean-stack@2.2.0: {} + cli-color@2.0.4: dependencies: d: 1.0.2 @@ -4657,6 +4821,12 @@ snapshots: memoizee: 0.4.15 timers-ext: 0.1.7 + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -4683,6 +4853,8 @@ snapshots: commander@9.5.0: {} + commondir@1.0.1: {} + concat-map@0.0.1: {} content-disposition@0.5.4: @@ -4691,6 +4863,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} cookie-signature@1.0.6: {} @@ -4780,6 +4954,8 @@ snapshots: optionalDependencies: supports-color: 5.5.0 + decamelize@1.2.0: {} + dedent@1.5.3: {} deep-eql@4.1.3: @@ -4790,6 +4966,10 @@ snapshots: deepmerge@4.3.1: {} + default-require-extensions@3.0.1: + dependencies: + strip-bom: 4.0.0 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.0 @@ -4951,6 +5131,8 @@ snapshots: esniff: 2.0.1 next-tick: 1.1.0 + es6-error@4.1.1: {} + es6-iterator@2.0.3: dependencies: d: 1.0.2 @@ -5271,6 +5453,12 @@ snapshots: transitivePeerDependencies: - supports-color + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -5293,10 +5481,17 @@ snapshots: dependencies: is-callable: 1.2.7 + foreground-child@2.0.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 3.0.7 + forwarded@0.2.0: {} fresh@0.5.2: {} + fromentries@1.3.2: {} + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -5417,6 +5612,11 @@ snapshots: dependencies: has-symbols: 1.0.3 + hasha@5.2.2: + dependencies: + is-stream: 2.0.1 + type-fest: 0.8.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -5463,6 +5663,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -5569,16 +5771,24 @@ snapshots: dependencies: which-typed-array: 1.1.15 + is-typedarray@1.0.0: {} + is-weakref@1.0.2: dependencies: call-bind: 1.0.7 + is-windows@1.0.2: {} + isarray@2.0.5: {} isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} + istanbul-lib-hook@3.0.0: + dependencies: + append-transform: 2.0.0 + istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.24.5 @@ -5599,6 +5809,15 @@ snapshots: transitivePeerDependencies: - supports-color + istanbul-lib-processinfo@2.0.3: + dependencies: + archy: 1.0.0 + cross-spawn: 7.0.3 + istanbul-lib-coverage: 3.2.2 + p-map: 3.0.0 + rimraf: 3.0.2 + uuid: 8.3.2 + istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 @@ -5999,6 +6218,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.flattendeep@4.4.0: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} @@ -6023,6 +6244,10 @@ snapshots: dependencies: es5-ext: 0.10.64 + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + make-dir@4.0.0: dependencies: semver: 7.6.2 @@ -6103,6 +6328,10 @@ snapshots: node-int64@0.4.0: {} + node-preload@0.2.1: + dependencies: + process-on-spawn: 1.0.0 + node-releases@2.0.14: {} normalize-package-data@2.5.0: @@ -6130,6 +6359,38 @@ snapshots: dependencies: path-key: 3.1.1 + nyc@17.0.0: + dependencies: + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + caching-transform: 4.0.0 + convert-source-map: 1.9.0 + decamelize: 1.2.0 + find-cache-dir: 3.3.2 + find-up: 4.1.0 + foreground-child: 2.0.0 + get-package-type: 0.1.0 + glob: 7.2.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-hook: 3.0.0 + istanbul-lib-instrument: 6.0.2 + istanbul-lib-processinfo: 2.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + make-dir: 3.1.0 + node-preload: 0.2.1 + p-map: 3.0.0 + process-on-spawn: 1.0.0 + resolve-from: 5.0.0 + rimraf: 3.0.2 + signal-exit: 3.0.7 + spawn-wrap: 2.0.0 + test-exclude: 6.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - supports-color + object-assign@4.1.1: {} object-inspect@1.13.1: {} @@ -6184,8 +6445,19 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@3.0.0: + dependencies: + aggregate-error: 3.1.0 + p-try@2.2.0: {} + package-hash@4.0.0: + dependencies: + graceful-fs: 4.2.11 + hasha: 5.2.2 + lodash.flattendeep: 4.4.0 + release-zalgo: 1.0.0 + pako@2.1.0: {} parent-module@1.0.1: @@ -6371,6 +6643,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + process-on-spawn@1.0.0: + dependencies: + fromentries: 1.3.2 + process-warning@3.0.0: {} process@0.11.10: {} @@ -6456,8 +6732,14 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 + release-zalgo@1.0.0: + dependencies: + es6-error: 4.1.1 + require-directory@2.1.1: {} + require-main-filename@2.0.0: {} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -6546,6 +6828,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-blocking@2.0.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -6609,6 +6893,15 @@ snapshots: source-map@0.6.1: {} + spawn-wrap@2.0.0: + dependencies: + foreground-child: 2.0.0 + is-windows: 1.0.2 + make-dir: 3.1.0 + rimraf: 3.0.2 + signal-exit: 3.0.7 + which: 2.0.2 + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -6786,6 +7079,8 @@ snapshots: type-fest@0.21.3: {} + type-fest@0.8.1: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -6825,6 +7120,10 @@ snapshots: is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + typescript@5.5.2: {} unbox-primitive@1.0.2: @@ -6895,6 +7194,8 @@ snapshots: is-string: 1.0.7 is-symbol: 1.0.4 + which-module@2.0.1: {} + which-typed-array@1.1.15: dependencies: available-typed-arrays: 1.0.7 @@ -6915,6 +7216,12 @@ snapshots: wordwrap@1.0.0: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -6923,6 +7230,13 @@ snapshots: wrappy@1.0.2: {} + write-file-atomic@3.0.3: + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + write-file-atomic@4.0.2: dependencies: imurmurhash: 0.1.4 @@ -6930,12 +7244,33 @@ snapshots: xtend@4.0.2: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@21.1.1: {} + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yargs@17.7.2: dependencies: cliui: 8.0.1 diff --git a/scripts/db/seed.ts b/scripts/db/seed.ts index b2b3e929..9a3f77f1 100644 --- a/scripts/db/seed.ts +++ b/scripts/db/seed.ts @@ -1,6 +1,5 @@ +import { cleanup, createDbClient, seed } from '../../src/db'; import { environmentVariables } from '../../src/types'; -import { createDbClient } from '../../src/utils/db/create-db-connection'; -import { cleanup, seed } from '../../src/utils/db/seed'; import { logger } from '../../src/utils/logger'; async function main() { diff --git a/src/utils/db/create-db-connection.ts b/src/db/create-db-connection.ts similarity index 88% rename from src/utils/db/create-db-connection.ts rename to src/db/create-db-connection.ts index 263278f3..4bc1ab31 100644 --- a/src/utils/db/create-db-connection.ts +++ b/src/db/create-db-connection.ts @@ -1,7 +1,7 @@ import { drizzle } from 'drizzle-orm/node-postgres'; import { Client, Pool } from 'pg'; -import * as db from '../../db'; -import { logger } from '../logger'; +import * as schema from '../db/schema'; +import { logger } from '../utils/logger'; /** * creates a postgres database pool connection @@ -37,7 +37,7 @@ export function createDbPool({ return { pool, - db: drizzle(pool, { schema: db }), + db: drizzle(pool, { schema }), }; } @@ -69,6 +69,6 @@ export async function createDbClient({ await client.connect(); return { client, - db: drizzle(client, { schema: db }), + db: drizzle(client, { schema }), }; } diff --git a/src/db/index.ts b/src/db/index.ts index b8c5be0e..95236f7b 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,21 +1,4 @@ -export * from './users'; -export * from './federated-credentials'; -export * from './registrations'; -export * from './cycles'; -export * from './questions'; -export * from './registration-field-options'; -export * from './options'; -export * from './votes'; -export * from './events'; -export * from './registration-fields'; -export * from './registration-data'; -export * from './groups'; -export * from './users-to-groups'; -export * from './user-attributes'; -export * from './comments'; -export * from './likes'; -export * from './notification-types'; -export * from './users-to-notifications'; -export * from './group-categories'; -export * from './questions-to-group-categories'; -export * from './alerts'; +export * from './create-db-connection'; +export * from './run-migrations'; +export * from './seed'; +export * from './test'; diff --git a/src/utils/db/run-migrations.ts b/src/db/run-migrations.ts similarity index 100% rename from src/utils/db/run-migrations.ts rename to src/db/run-migrations.ts diff --git a/src/db/alerts.ts b/src/db/schema/alerts.ts similarity index 100% rename from src/db/alerts.ts rename to src/db/schema/alerts.ts diff --git a/src/db/comments.ts b/src/db/schema/comments.ts similarity index 100% rename from src/db/comments.ts rename to src/db/schema/comments.ts diff --git a/src/db/cycles.ts b/src/db/schema/cycles.ts similarity index 100% rename from src/db/cycles.ts rename to src/db/schema/cycles.ts diff --git a/src/db/events.ts b/src/db/schema/events.ts similarity index 100% rename from src/db/events.ts rename to src/db/schema/events.ts diff --git a/src/db/federated-credentials.ts b/src/db/schema/federated-credentials.ts similarity index 100% rename from src/db/federated-credentials.ts rename to src/db/schema/federated-credentials.ts diff --git a/src/db/group-categories.ts b/src/db/schema/group-categories.ts similarity index 100% rename from src/db/group-categories.ts rename to src/db/schema/group-categories.ts diff --git a/src/db/groups.ts b/src/db/schema/groups.ts similarity index 100% rename from src/db/groups.ts rename to src/db/schema/groups.ts diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts new file mode 100644 index 00000000..b8c5be0e --- /dev/null +++ b/src/db/schema/index.ts @@ -0,0 +1,21 @@ +export * from './users'; +export * from './federated-credentials'; +export * from './registrations'; +export * from './cycles'; +export * from './questions'; +export * from './registration-field-options'; +export * from './options'; +export * from './votes'; +export * from './events'; +export * from './registration-fields'; +export * from './registration-data'; +export * from './groups'; +export * from './users-to-groups'; +export * from './user-attributes'; +export * from './comments'; +export * from './likes'; +export * from './notification-types'; +export * from './users-to-notifications'; +export * from './group-categories'; +export * from './questions-to-group-categories'; +export * from './alerts'; diff --git a/src/db/likes.ts b/src/db/schema/likes.ts similarity index 100% rename from src/db/likes.ts rename to src/db/schema/likes.ts diff --git a/src/db/notification-types.ts b/src/db/schema/notification-types.ts similarity index 100% rename from src/db/notification-types.ts rename to src/db/schema/notification-types.ts diff --git a/src/db/options.ts b/src/db/schema/options.ts similarity index 100% rename from src/db/options.ts rename to src/db/schema/options.ts diff --git a/src/db/questions-to-group-categories.ts b/src/db/schema/questions-to-group-categories.ts similarity index 100% rename from src/db/questions-to-group-categories.ts rename to src/db/schema/questions-to-group-categories.ts diff --git a/src/db/questions.ts b/src/db/schema/questions.ts similarity index 100% rename from src/db/questions.ts rename to src/db/schema/questions.ts diff --git a/src/db/registration-data.ts b/src/db/schema/registration-data.ts similarity index 100% rename from src/db/registration-data.ts rename to src/db/schema/registration-data.ts diff --git a/src/db/registration-field-options.ts b/src/db/schema/registration-field-options.ts similarity index 100% rename from src/db/registration-field-options.ts rename to src/db/schema/registration-field-options.ts diff --git a/src/db/registration-fields.ts b/src/db/schema/registration-fields.ts similarity index 100% rename from src/db/registration-fields.ts rename to src/db/schema/registration-fields.ts diff --git a/src/db/registrations.ts b/src/db/schema/registrations.ts similarity index 100% rename from src/db/registrations.ts rename to src/db/schema/registrations.ts diff --git a/src/db/user-attributes.ts b/src/db/schema/user-attributes.ts similarity index 100% rename from src/db/user-attributes.ts rename to src/db/schema/user-attributes.ts diff --git a/src/db/users-to-groups.ts b/src/db/schema/users-to-groups.ts similarity index 100% rename from src/db/users-to-groups.ts rename to src/db/schema/users-to-groups.ts diff --git a/src/db/users-to-notifications.ts b/src/db/schema/users-to-notifications.ts similarity index 100% rename from src/db/users-to-notifications.ts rename to src/db/schema/users-to-notifications.ts diff --git a/src/db/users.ts b/src/db/schema/users.ts similarity index 100% rename from src/db/users.ts rename to src/db/schema/users.ts diff --git a/src/db/votes.ts b/src/db/schema/votes.ts similarity index 100% rename from src/db/votes.ts rename to src/db/schema/votes.ts diff --git a/src/db/seed.ts b/src/db/seed.ts new file mode 100644 index 00000000..a812a194 --- /dev/null +++ b/src/db/seed.ts @@ -0,0 +1,515 @@ +import { + randBasketballTeam, + randBook, + randCity, + randDog, + randEmail, + randFirstName, + randJobTitle, + randLastName, + randUserName, + randUuid, +} from '@ngneat/falso'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { createInsertSchema } from 'drizzle-zod'; +import { z } from 'zod'; +import { fieldsSchema, insertOptionsSchema } from '../types'; +import * as schema from './schema'; + +// Define the data types for the seed function +const insertCycleSchema = createInsertSchema(schema.cycles); +const insertEventSchema = createInsertSchema(schema.events, { + fields: fieldsSchema, +}); +const insertGroupCategoriesSchema = createInsertSchema(schema.groupCategories); +const insertGroupsSchema = createInsertSchema(schema.groups); +const insertQuestionsSchema = createInsertSchema(schema.questions, { + fields: fieldsSchema, +}); +const insertQuestionsToGroupCategoriesSchema = createInsertSchema( + schema.questionsToGroupCategories, +); +const insertUsersSchema = createInsertSchema(schema.users); +const insertUsersToGroupsSchema = createInsertSchema(schema.usersToGroups); + +async function seed(dbPool: NodePgDatabase) { + const events = await createEvent(dbPool, [ + { + name: randCity(), + fields: [ + { + id: randUuid(), + name: 'submit project', + type: 'TEXT', + position: 0, + validation: { + required: true, + }, + }, + ], + }, + ]); + const cycles = await createCycle(dbPool, [ + { + startAt: new Date(), + // end in 5 mins + endAt: new Date(Date.now() + 300000), + status: 'OPEN', + eventId: events[0]!.id, + }, + ]); + + const forumQuestions = await createQuestions(dbPool, [ + { + cycleId: cycles[0]!.id, + title: 'What do you think about this event?', + voteModel: 'COCM', + }, + { + cycleId: cycles[0]!.id, + title: 'How do you feel about this event?', + voteModel: 'QV', + }, + ]); + const questionOptions = await createQuestionOptions(dbPool, [ + { + questionId: forumQuestions[0]!.id, + title: 'Great', + show: false, + }, + { + questionId: forumQuestions[0]!.id, + title: 'Good', + show: true, + }, + { + questionId: forumQuestions[0]!.id, + title: 'Bad', + show: true, + }, + ]); + + const groupCategories = await createGroupCategories(dbPool, [ + { eventId: events[0]!.id, name: 'affiliation', userCanView: true, required: true }, + { eventId: events[0]!.id, name: 'public', userCanView: true, userCanCreate: false }, + { eventId: events[0]!.id, name: 'secrets', userCanCreate: true, userCanView: false }, + { + eventId: events[0]!.id, + name: 'tension', + userCanCreate: true, + userCanView: true, + required: false, + }, + ]); + + const groups = await createGroups(dbPool, [ + // 5 groups in first category + { + name: randBasketballTeam(), + groupCategoryId: groupCategories[0]!.id, + }, + { + name: randBasketballTeam(), + groupCategoryId: groupCategories[0]!.id, + }, + { + name: randBasketballTeam(), + groupCategoryId: groupCategories[0]!.id, + }, + { + name: randBasketballTeam(), + groupCategoryId: groupCategories[0]!.id, + }, + { + name: randBasketballTeam(), + groupCategoryId: groupCategories[0]!.id, + }, + // 4 groups in second category + { + name: randBook().title, + groupCategoryId: groupCategories[1]!.id, + }, + { + name: randBook().title, + groupCategoryId: groupCategories[1]!.id, + }, + { + name: randBook().title, + groupCategoryId: groupCategories[1]!.id, + }, + { + name: randBook().title, + groupCategoryId: groupCategories[1]!.id, + }, + // 3 groups in third category + { + name: randJobTitle(), + groupCategoryId: groupCategories[2]!.id, + }, + { + name: randJobTitle(), + groupCategoryId: groupCategories[2]!.id, + }, + { + name: randJobTitle(), + groupCategoryId: groupCategories[2]!.id, + }, + // 3 groups in fourth category + { + name: randDog(), + groupCategoryId: groupCategories[3]!.id, + }, + { + name: randDog(), + groupCategoryId: groupCategories[3]!.id, + }, + { + name: randDog(), + groupCategoryId: groupCategories[3]!.id, + }, + ]); + + const users = await createUsers(dbPool, [ + // 3 random users + { + email: randEmail(), + username: randUserName(), + firstName: randFirstName(), + lastName: randLastName(), + }, + { + email: randEmail(), + username: randUserName(), + firstName: randFirstName(), + lastName: randLastName(), + }, + { + email: randEmail(), + username: randUserName(), + firstName: randFirstName(), + lastName: randLastName(), + }, + ]); + + const usersToGroups = await createUsersToGroups( + dbPool, + // user0 => [group0, group1] + // user1 => [group0, group1] + // user2 => [group0, group2] + [ + // user1 + { + userId: users[0]!.id, + groupId: groups[0]!.id, + groupCategoryId: groups[0]!.groupCategoryId, + }, + { + userId: users[0]!.id, + groupId: groups[1]!.id, + groupCategoryId: groups[1]!.groupCategoryId, + }, + // user2 + { + userId: users[1]!.id, + groupId: groups[0]!.id, + groupCategoryId: groups[0]!.groupCategoryId, + }, + { + userId: users[1]!.id, + groupId: groups[1]!.id, + groupCategoryId: groups[1]!.groupCategoryId, + }, + // user3 + { + userId: users[2]!.id, + groupId: groups[0]!.id, + groupCategoryId: groups[0]!.groupCategoryId, + }, + { + userId: users[2]!.id, + groupId: groups[2]!.id, + groupCategoryId: groups[2]!.groupCategoryId, + }, + ], + ); + + const questionsToGroupCategories = await createQuestionsToGroupCategories(dbPool, [ + { + questionId: forumQuestions[0]!.id, + groupCategoryId: groupCategories[0]!.id, + }, + ]); + + return { + events, + cycles, + forumQuestions, + questionOptions, + groupCategories, + groups, + users, + usersToGroups, + questionsToGroupCategories, + }; +} + +async function cleanup(dbPool: NodePgDatabase) { + await dbPool.delete(schema.userAttributes); + await dbPool.delete(schema.votes); + await dbPool.delete(schema.federatedCredentials); + await dbPool.delete(schema.options); + await dbPool.delete(schema.registrationData); + await dbPool.delete(schema.registrationFieldOptions); + await dbPool.delete(schema.registrationFields); + await dbPool.delete(schema.registrations); + await dbPool.delete(schema.usersToGroups); + await dbPool.delete(schema.users); + await dbPool.delete(schema.groups); + await dbPool.delete(schema.questionsToGroupCategories); + await dbPool.delete(schema.groupCategories); + await dbPool.delete(schema.questions); + await dbPool.delete(schema.cycles); + await dbPool.delete(schema.events); +} + +async function createEvent( + dbPool: NodePgDatabase, + eventData: z.infer[], +) { + const events = []; + for (const event of eventData) { + const result = await dbPool + .insert(schema.events) + .values({ + name: event.name, + fields: event.fields, + }) + .returning(); + events.push(result[0]); + } + return events; +} + +async function createCycle( + dbPool: NodePgDatabase, + cycleData: z.infer[], +) { + if (cycleData.length === 0) { + throw new Error('Cycle data is empty.'); + } + + const cycles = []; + for (const cycle of cycleData) { + if (!cycle.eventId) { + throw new Error('Event ID is not defined.'); + } + + const result = await dbPool + .insert(schema.cycles) + .values({ + startAt: cycle.startAt, + endAt: cycle.endAt, + status: cycle.status, + eventId: cycle.eventId, + }) + .returning(); + + cycles.push(result[0]); + } + + return cycles; +} + +async function createQuestions( + dbPool: NodePgDatabase, + questionData: z.infer[], +) { + if (questionData.length === 0) { + throw new Error('Forum Question data is empty.'); + } + + const questions = []; + for (const question of questionData) { + const result = await dbPool + .insert(schema.questions) + .values({ + cycleId: question.cycleId, + title: question.title, + voteModel: question.voteModel, + }) + .returning(); + + questions.push(result[0]); + } + + return questions; +} + +async function createQuestionOptions( + dbPool: NodePgDatabase, + optionData: z.infer[], +) { + if (optionData.length === 0) { + throw new Error('Question Option data is empty.'); + } + + const options = []; + for (const option of optionData) { + if (!option.questionId) { + throw new Error('Question ID is not defined for the question option.'); + } + + const result = await dbPool + .insert(schema.options) + .values({ + questionId: option.questionId, + title: option.title, + show: option.show, + }) + .returning(); + + options.push(result[0]); + } + + return options; +} + +async function createGroupCategories( + dbPool: NodePgDatabase, + groupCategoriesData: z.infer[], +) { + if (groupCategoriesData.length === 0) { + throw new Error('Group Categories data is empty.'); + } + + const groupCategories = []; + for (const data of groupCategoriesData) { + if (!data.eventId) { + throw new Error('Event ID is not defined for the group category.'); + } + + const result = await dbPool + .insert(schema.groupCategories) + .values({ + name: data.name, + eventId: data.eventId, + userCanCreate: data.userCanCreate, + userCanView: data.userCanView, + required: data.required, + }) + .returning(); + + groupCategories.push(result[0]); + } + + return groupCategories; +} + +async function createGroups( + dbPool: NodePgDatabase, + groupData: z.infer[], +) { + if (groupData.length === 0) { + throw new Error('Group Data is empty.'); + } + + const groups = []; + for (const group of groupData) { + if (!group.groupCategoryId) { + throw new Error('Group Category ID is not defined for the group.'); + } + + const result = await dbPool + .insert(schema.groups) + .values({ + name: group.name, + groupCategoryId: group.groupCategoryId, + }) + .returning(); + + groups.push(result[0]); + } + + return groups; +} + +async function createUsers( + dbPool: NodePgDatabase, + userData: z.infer[], +) { + const users = []; + for (const user of userData) { + const result = await dbPool + .insert(schema.users) + .values({ + username: user.username, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + }) + .returning(); + + users.push(result[0]); + } + + return users; +} + +async function createUsersToGroups( + dbPool: NodePgDatabase, + usersToGroupsData: z.infer[], +) { + if (usersToGroupsData.length === 0) { + throw new Error('Users to Groups Data is empty.'); + } + + const usersToGroups = []; + for (const group of usersToGroupsData) { + if (!group.groupId) { + throw new Error('Group ID is not defined for the users to groups relationship.'); + } + + const result = await dbPool + .insert(schema.usersToGroups) + .values({ + userId: group.userId, + groupId: group.groupId, + groupCategoryId: group.groupCategoryId, + }) + .returning(); + + usersToGroups.push(result[0]); + } + + return usersToGroups; +} + +async function createQuestionsToGroupCategories( + dbPool: NodePgDatabase, + questionsToGroupCategoriesData: z.infer[], +) { + if (questionsToGroupCategoriesData.length === 0) { + throw new Error('Questions to Group Categories Data is empty.'); + } + + const questionsToGroupCategories = []; + for (const groupCategories of questionsToGroupCategoriesData) { + if (!groupCategories.questionId) { + throw new Error('Question ID is not defined for the group Category.'); + } + + const result = await dbPool + .insert(schema.questionsToGroupCategories) + .values({ + questionId: groupCategories.questionId, + groupCategoryId: groupCategories.groupCategoryId, + }) + .returning(); + + questionsToGroupCategories.push(result[0]); + } + + return questionsToGroupCategories; +} + +export { cleanup, seed }; diff --git a/src/db/test.ts b/src/db/test.ts new file mode 100644 index 00000000..0479baeb --- /dev/null +++ b/src/db/test.ts @@ -0,0 +1,80 @@ +import { z } from 'zod'; +import { environmentVariables } from '../types'; +import { Client } from 'pg'; +import { createDbClient } from './create-db-connection'; +import { runMigrations } from './run-migrations'; + +export async function createTestDatabase(envVariables: z.infer) { + const initDb = await createDbClient({ + database: envVariables.DATABASE_NAME, // Use the original database name here + host: envVariables.DATABASE_HOST, + password: envVariables.DATABASE_PASSWORD, + user: envVariables.DATABASE_USER, + port: envVariables.DATABASE_PORT, + }); + + // use node postgres to create a random new + // database for testing + const testDbName = createTestDatabaseName(envVariables.DATABASE_NAME); + await initDb.client.query(`CREATE DATABASE "${testDbName}"`); + // disconnect from the original database + await initDb.client.end(); + // connect to the new database + const newClient = await createDbClient({ + database: testDbName, + host: envVariables.DATABASE_HOST, + password: envVariables.DATABASE_PASSWORD, + user: envVariables.DATABASE_USER, + port: envVariables.DATABASE_PORT, + }); + + // run migrations + await runMigrations({ + database: testDbName, + host: envVariables.DATABASE_HOST, + password: envVariables.DATABASE_PASSWORD, + user: envVariables.DATABASE_USER, + port: envVariables.DATABASE_PORT, + }); + + return { + dbClient: newClient, + teardown: async () => { + await teardownTestDatabase(newClient.client, testDbName); + }, + }; +} + +function createTestDatabaseName(name: string) { + return `${name}_test_${Math.random().toString(36).substring(7)}`; +} + +async function teardownTestDatabase(client: Client, name: string) { + await client.end(); + + // connect to the default 'postgres' database + const dropClient = new Client({ + host: client.host, + port: client.port, + user: client.user, + password: client.password, + database: 'postgres', // Connect to the default database + }); + + await dropClient.connect(); + + // Terminate all connections to the test database + await dropClient.query( + ` + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = $1 + AND pid <> pg_backend_pid(); + `, + [name], + ); + + await dropClient.query(`DROP DATABASE IF EXISTS ${name}`); + + await dropClient.end(); +} diff --git a/src/handlers/alerts.ts b/src/handlers/alerts.ts index aebb890a..64989817 100644 --- a/src/handlers/alerts.ts +++ b/src/handlers/alerts.ts @@ -1,16 +1,16 @@ import type { Request, Response } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { and, eq, gte, lte, or } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { logger } from '../utils/logger'; -export function getActiveAlerts(dbPool: NodePgDatabase) { +export function getActiveAlerts(dbPool: NodePgDatabase) { 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())), + eq(schema.alerts.active, true), + and(lte(schema.alerts.startAt, new Date()), gte(schema.alerts.endAt, new Date())), ), }); diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts index fd3717a0..c939b635 100644 --- a/src/handlers/auth.ts +++ b/src/handlers/auth.ts @@ -1,6 +1,6 @@ import type { Request, Response } from 'express'; import { SemaphoreSignaturePCDPackage } from '@pcd/semaphore-signature-pcd'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { createOrSignInPCD } from '../services/auth'; import { verifyUserSchema } from '../types'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; @@ -13,7 +13,7 @@ export function destroySessionHandler() { }; } -export function verifyPCDHandler(dbPool: NodePgDatabase) { +export function verifyPCDHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { try { const body = verifyUserSchema.safeParse(req.body); diff --git a/src/handlers/comments.ts b/src/handlers/comments.ts index 44af5112..5c1f5a08 100644 --- a/src/handlers/comments.ts +++ b/src/handlers/comments.ts @@ -1,5 +1,5 @@ import type { Request, Response } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { eq } from 'drizzle-orm'; import { deleteCommentLike, saveCommentLike, userCanLike } from '../services/likes'; import { insertCommentSchema } from '../types'; @@ -7,7 +7,7 @@ import { deleteComment, saveComment, userCanComment } from '../services/comments import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { logger } from '../utils/logger'; -export function getCommentLikesHandler(dbPool: NodePgDatabase) { +export function getCommentLikesHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const commentId = req.params.commentId; @@ -16,14 +16,14 @@ export function getCommentLikesHandler(dbPool: NodePgDatabase) { } const likes = await dbPool.query.likes.findMany({ - where: eq(db.likes.commentId, commentId), + where: eq(schema.likes.commentId, commentId), }); return res.json({ data: likes }); }; } -export function saveCommentLikeHandler(dbPool: NodePgDatabase) { +export function saveCommentLikeHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const commentId = req.params.commentId; const userId = req.session.userId; @@ -48,7 +48,7 @@ export function saveCommentLikeHandler(dbPool: NodePgDatabase) { }; } -export function deleteCommentLikeHandler(dbPool: NodePgDatabase) { +export function deleteCommentLikeHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const commentId = req.params.commentId; const userId = req.session.userId; @@ -77,7 +77,7 @@ export function deleteCommentLikeHandler(dbPool: NodePgDatabase) { * @param { NodePgDatabase} dbPool - The database pool connection. * @returns {Promise} - A promise that resolves once the comment is saved. */ -export function saveCommentHandler(dbPool: NodePgDatabase) { +export function saveCommentHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const userId = req.session.userId; const body = insertCommentSchema.safeParse(req.body); @@ -108,7 +108,7 @@ export function saveCommentHandler(dbPool: NodePgDatabase) { * @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 deleteCommentHandler(dbPool: NodePgDatabase) { +export function deleteCommentHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const commentId = req.params.commentId; const userId = req.session.userId; diff --git a/src/handlers/cycles.ts b/src/handlers/cycles.ts index b9dcb2c4..83d65090 100644 --- a/src/handlers/cycles.ts +++ b/src/handlers/cycles.ts @@ -1,13 +1,13 @@ import { and, eq, gte, lte } from 'drizzle-orm'; import type { Request, Response } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { GetCycleById, getCycleVotes } from '../services/cycles'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -export function getActiveCyclesHandler(dbPool: NodePgDatabase) { +export function getActiveCyclesHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const activeCycles = await dbPool.query.cycles.findMany({ - where: and(lte(db.cycles.startAt, new Date()), gte(db.cycles.endAt, new Date())), + where: and(lte(schema.cycles.startAt, new Date()), gte(schema.cycles.endAt, new Date())), with: { questions: { with: { @@ -15,7 +15,7 @@ export function getActiveCyclesHandler(dbPool: NodePgDatabase) { columns: { voteScore: false, }, - where: eq(db.options.show, true), + where: eq(schema.options.show, true), }, }, }, @@ -26,7 +26,7 @@ export function getActiveCyclesHandler(dbPool: NodePgDatabase) { }; } -export function getCycleHandler(dbPool: NodePgDatabase) { +export function getCycleHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const { cycleId } = req.params; @@ -42,11 +42,8 @@ export function getCycleHandler(dbPool: NodePgDatabase) { /** * Handler to receive the votes for a specific cycle and user. - * @param { NodePgDatabase} dbPool - The database connection pool. - * @param {Request} req - The Express request object. - * @param {Response} res - The Express response object. */ -export function getCycleVotesHandler(dbPool: NodePgDatabase) { +export function getCycleVotesHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const userId = req.session.userId; const cycleId = req.params.cycleId; diff --git a/src/handlers/events.ts b/src/handlers/events.ts index e3da0571..e58f05b1 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -1,10 +1,10 @@ import { and, eq } from 'drizzle-orm'; import type { Request, Response } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { logger } from '../utils/logger'; -export function getEventCyclesHandler(dbPool: NodePgDatabase) { +export function getEventCyclesHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const { eventId } = req.params; @@ -13,7 +13,7 @@ export function getEventCyclesHandler(dbPool: NodePgDatabase) { } const eventCycles = await dbPool.query.cycles.findMany({ - where: eq(db.cycles.eventId, eventId), + where: eq(schema.cycles.eventId, eventId), with: { questions: { with: { @@ -21,7 +21,7 @@ export function getEventCyclesHandler(dbPool: NodePgDatabase) { columns: { voteScore: false, }, - where: eq(db.options.show, true), + where: eq(schema.options.show, true), }, }, }, @@ -32,7 +32,7 @@ export function getEventCyclesHandler(dbPool: NodePgDatabase) { }; } -export function getEventGroupCategoriesHandler(dbPool: NodePgDatabase) { +export function getEventGroupCategoriesHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const { eventId } = req.params; @@ -41,21 +41,21 @@ export function getEventGroupCategoriesHandler(dbPool: NodePgDatabase } const eventGroupCategories = await dbPool.query.groupCategories.findMany({ - where: eq(db.groupCategories.eventId, eventId), + where: eq(schema.groupCategories.eventId, eventId), }); return res.json({ data: eventGroupCategories }); }; } -export function getEventsHandler(dbPool: NodePgDatabase) { +export function getEventsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const events = await dbPool.query.events.findMany(); return res.json({ data: events }); }; } -export function getEventHandler(dbPool: NodePgDatabase) { +export function getEventHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const { eventId } = req.params; @@ -64,14 +64,14 @@ export function getEventHandler(dbPool: NodePgDatabase) { } const event = await dbPool.query.events.findFirst({ - where: eq(db.events.id, eventId), + where: eq(schema.events.id, eventId), }); return res.json({ data: event }); }; } -export function getEventRegistrationFieldsHandler(dbPool: NodePgDatabase) { +export function getEventRegistrationFieldsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const eventId = req.params.eventId; if (!eventId) { @@ -86,14 +86,14 @@ export function getEventRegistrationFieldsHandler(dbPool: NodePgDatabase) { +export function getEventRegistrationsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { // parse input const eventId = req.params.eventId ?? ''; @@ -101,7 +101,10 @@ export function getEventRegistrationsHandler(dbPool: NodePgDatabase) try { const out = await dbPool.query.registrations.findMany({ - where: and(eq(db.registrations.userId, userId), eq(db.registrations.eventId, eventId)), + where: and( + eq(schema.registrations.userId, userId), + eq(schema.registrations.eventId, eventId), + ), }); return res.json({ data: out }); diff --git a/src/handlers/group-categories.ts b/src/handlers/group-categories.ts index a6bef307..ee24435f 100644 --- a/src/handlers/group-categories.ts +++ b/src/handlers/group-categories.ts @@ -1,17 +1,17 @@ import type { Request, Response } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { and, eq } from 'drizzle-orm'; import { canViewGroupsInGroupCategory } from '../services/group-categories'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -export function getGroupCategoriesHandler(dbPool: NodePgDatabase) { +export function getGroupCategoriesHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const groupCategories = await dbPool.query.groupCategories.findMany(); return res.json({ data: groupCategories }); }; } -export function getGroupCategoryHandler(dbPool: NodePgDatabase) { +export function getGroupCategoryHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const groupCategoryId = req.params.id; @@ -20,14 +20,14 @@ export function getGroupCategoryHandler(dbPool: NodePgDatabase) { } const groupCategory = await dbPool.query.groupCategories.findFirst({ - where: and(eq(db.groupCategories.id, groupCategoryId)), + where: and(eq(schema.groupCategories.id, groupCategoryId)), }); return res.json({ data: groupCategory }); }; } -export function getGroupCategoriesGroupsHandler(dbPool: NodePgDatabase) { +export function getGroupCategoriesGroupsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const groupCategoryId = req.params.id; @@ -36,7 +36,7 @@ export function getGroupCategoriesGroupsHandler(dbPool: NodePgDatabase) { +export function createGroupHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const userId = req.session.userId; const body = insertGroupsSchema.safeParse(req.body); @@ -46,14 +46,8 @@ export function createGroupHandler(dbPool: NodePgDatabase) { /** * Retrieves author and co-author data for a given question option created as a secret group. - * - * @param { NodePgDatabase} 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 author data. - */ -export function getGroupMembersHandler(dbPool: NodePgDatabase) { + * */ +export function getGroupMembersHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { try { const groupId = req.params.id; @@ -77,14 +71,8 @@ export function getGroupMembersHandler(dbPool: NodePgDatabase) { /** * Retrieves group registration data of a secret group for a given group Id. - * - * @param { NodePgDatabase} 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 registration data. */ -export function getGroupRegistrationsHandler(dbPool: NodePgDatabase) { +export function getGroupRegistrationsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { try { const groupId = req.params.id; diff --git a/src/handlers/options.ts b/src/handlers/options.ts index b3915cf4..7c12304a 100644 --- a/src/handlers/options.ts +++ b/src/handlers/options.ts @@ -1,6 +1,6 @@ import { eq, getTableColumns } from 'drizzle-orm'; import type { Request, Response } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { getOptionUsers, getOptionComments } from '../services/comments'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { logger } from '../utils/logger'; @@ -14,7 +14,7 @@ import { validateOptionData, } from '../services/options'; -export function getOptionHandler(dbPool: NodePgDatabase) { +export function getOptionHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const { optionId } = req.params; @@ -22,14 +22,14 @@ export function getOptionHandler(dbPool: NodePgDatabase) { return res.status(400).json({ error: 'Missing optionId' }); } - const { voteScore, ...rest } = getTableColumns(db.options); + const { voteScore, ...rest } = getTableColumns(schema.options); const rows = await dbPool .select({ ...rest, }) - .from(db.options) - .where(eq(db.options.id, optionId)); + .from(schema.options) + .where(eq(schema.options.id, optionId)); if (!rows.length) { return res.status(404).json({ error: 'Option not found' }); @@ -41,10 +41,8 @@ export function getOptionHandler(dbPool: NodePgDatabase) { /** * Retrieves comments related to a specific question option from the database and associates them with corresponding user information. - * @param { NodePgDatabase} dbPool - The database pool connection. - * @returns {Promise} - A promise that resolves with the retrieved comments, each associated with user information if available. */ -export function getOptionCommentsHandler(dbPool: NodePgDatabase) { +export function getOptionCommentsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const optionId = req.params.optionId ?? ''; @@ -61,14 +59,8 @@ export function getOptionCommentsHandler(dbPool: NodePgDatabase) { /** * Retrieves author and co-author data for a given question option created as a secret group. - * - * @param { NodePgDatabase} 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 author data. */ -export function getOptionUsersHandler(dbPool: NodePgDatabase) { +export function getOptionUsersHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { try { const optionId = req.params.optionId; @@ -90,7 +82,7 @@ export function getOptionUsersHandler(dbPool: NodePgDatabase) { }; } -export function saveOptionHandler(dbPool: NodePgDatabase) { +export function saveOptionHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const userId = req.session.userId; const body = insertOptionsSchema.safeParse(req.body); @@ -137,7 +129,7 @@ export function saveOptionHandler(dbPool: NodePgDatabase) { }; } -export function updateOptionHandler(dbPool: NodePgDatabase) { +export function updateOptionHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const optionId = req.params.optionId; diff --git a/src/handlers/questions.ts b/src/handlers/questions.ts index ac6b5020..67a7d050 100644 --- a/src/handlers/questions.ts +++ b/src/handlers/questions.ts @@ -1,12 +1,12 @@ import type { Request, Response } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { getQuestionHearts } from '../services/questions'; import { executeResultQueries } from '../services/statistics'; import { calculateFunding } from '../services/funding-mechanism'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { logger } from '../utils/logger'; -export function getQuestionHeartsHandler(dbPool: NodePgDatabase) { +export function getQuestionHeartsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const forumQuestionId = req.params.forumQuestionId; @@ -22,14 +22,8 @@ export function getQuestionHeartsHandler(dbPool: NodePgDatabase) { /** * Retrieves result statistics for a specific forum question from the database. - * - * @param { NodePgDatabase} 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 getResultStatisticsHandler(dbPool: NodePgDatabase) { +export function getResultStatisticsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { try { const forumQuestionId = req.params.forumQuestionId; @@ -53,14 +47,8 @@ export function getResultStatisticsHandler(dbPool: NodePgDatabase) { /** * Retrieves result statistics for a specific forum question from the database. - * - * @param { NodePgDatabase} 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 getCalculateFundingHandler(dbPool: NodePgDatabase) { +export function getCalculateFundingHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { try { const forumQuestionId = req.params.forumQuestionId; diff --git a/src/handlers/registrations.ts b/src/handlers/registrations.ts index 87c9c516..cfe04bdf 100644 --- a/src/handlers/registrations.ts +++ b/src/handlers/registrations.ts @@ -1,5 +1,5 @@ import type { Request, Response } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { insertRegistrationSchema } from '../types'; import { saveRegistration, @@ -12,7 +12,7 @@ import { eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { logger } from '../utils/logger'; -export function getRegistrationDataHandler(dbPool: NodePgDatabase) { +export function getRegistrationDataHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const registrationId = req.params.id; const userId = req.session.userId; @@ -29,7 +29,7 @@ export function getRegistrationDataHandler(dbPool: NodePgDatabase) { with: { registrationData: true, }, - where: eq(db.registrations.id, registrationId), + where: eq(schema.registrations.id, registrationId), }); const out = [...(registration?.registrationData ?? [])]; @@ -41,7 +41,7 @@ export function getRegistrationDataHandler(dbPool: NodePgDatabase) { }; } -export function saveRegistrationHandler(dbPool: NodePgDatabase) { +export function saveRegistrationHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const userId = req.session.userId; req.body.userId = userId; @@ -80,7 +80,7 @@ export function saveRegistrationHandler(dbPool: NodePgDatabase) { }; } -export function updateRegistrationHandler(dbPool: NodePgDatabase) { +export function updateRegistrationHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const registrationId = req.params.id; diff --git a/src/handlers/users-to-groups.ts b/src/handlers/users-to-groups.ts index c1596c1c..3026743a 100644 --- a/src/handlers/users-to-groups.ts +++ b/src/handlers/users-to-groups.ts @@ -1,5 +1,5 @@ import type { Request, Response } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { joinGroupsSchema, leaveGroupsSchema, @@ -15,7 +15,7 @@ import { eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { logger } from '../utils/logger'; -export function joinGroupsHandler(dbPool: NodePgDatabase) { +export function joinGroupsHandler(dbPool: NodePgDatabase) { return async (req: Request, res: Response) => { const userId = req.session.userId; const body = joinGroupsSchema.safeParse(req.body); @@ -29,7 +29,7 @@ export function joinGroupsHandler(dbPool: NodePgDatabase) { // public group if ('groupId' in body.data) { const group = await dbPool.query.groups.findFirst({ - where: eq(db.groups.id, body.data.groupId), + where: eq(schema.groups.id, body.data.groupId), }); if (!group) { @@ -76,7 +76,7 @@ export function joinGroupsHandler(dbPool: NodePgDatabase) { }; } -export function updateGroupsHandler(dbPool: NodePgDatabase) { +export function updateGroupsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const userId = req.session.userId; const body = updateUsersToGroupsSchema.safeParse({ @@ -107,7 +107,7 @@ export function updateGroupsHandler(dbPool: NodePgDatabase) { }; } -export function leaveGroupsHandler(dbPool: NodePgDatabase) { +export function leaveGroupsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const userId = req.session.userId; const id = req.params.id; diff --git a/src/handlers/users.ts b/src/handlers/users.ts index bc0b2a98..07a76c21 100644 --- a/src/handlers/users.ts +++ b/src/handlers/users.ts @@ -1,6 +1,6 @@ import { eq } from 'drizzle-orm'; import type { Request, Response } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { updateUser } from '../services/users'; import { insertUserSchema } from '../types'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; @@ -8,15 +8,13 @@ import { logger } from '../utils/logger'; /** * Retrieves user data from the database. - * @param { NodePgDatabase} dbPool - The database connection pool. - * @returns {Function} - Express middleware function to handle the request. */ -export function getUserHandler(dbPool: NodePgDatabase) { +export function getUserHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { try { const userId = req.session.userId; const user = await dbPool.query.users.findFirst({ - where: eq(db.users.id, userId), + where: eq(schema.users.id, userId), }); if (!user) { @@ -33,10 +31,8 @@ export function getUserHandler(dbPool: NodePgDatabase) { /** * Updates user data in the database. - * @param { NodePgDatabase} dbPool - The database connection pool. - * @returns {Function} - Express middleware function to handle the request. */ -export function updateUserHandler(dbPool: NodePgDatabase) { +export function updateUserHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const queryUserId = req.params.userId; const userId = req.session.userId; @@ -83,10 +79,8 @@ export function updateUserHandler(dbPool: NodePgDatabase) { /** * Retrieves groups associated with a specific user. - * @param dbPool The database connection pool. - * @returns An asynchronous function that handles the HTTP request and response. */ -export function getUsersToGroupsHandler(dbPool: NodePgDatabase) { +export function getUsersToGroupsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const paramsUserId = req.params.userId; const userId = req.session.userId; @@ -102,7 +96,7 @@ export function getUsersToGroupsHandler(dbPool: NodePgDatabase) { }, }, }, - where: eq(db.usersToGroups.userId, userId), + where: eq(schema.usersToGroups.userId, userId), }); return res.json({ data: query }); @@ -115,10 +109,8 @@ export function getUsersToGroupsHandler(dbPool: NodePgDatabase) { /** * Retrieves user attributes from the database. - * @param { NodePgDatabase} dbPool - The database connection pool. - * @returns {Function} - Express middleware function to handle the request. */ -export function getUserAttributesHandler(dbPool: NodePgDatabase) { +export function getUserAttributesHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { try { const userId = req.session.userId; @@ -135,7 +127,7 @@ export function getUserAttributesHandler(dbPool: NodePgDatabase) { } const userAttributes = await dbPool.query.userAttributes.findMany({ - where: eq(db.userAttributes.userId, userId), + where: eq(schema.userAttributes.userId, userId), }); return res.json({ data: userAttributes }); @@ -146,7 +138,7 @@ export function getUserAttributesHandler(dbPool: NodePgDatabase) { }; } -export function getUserOptionsHandler(dbPool: NodePgDatabase) { +export function getUserOptionsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const userId = req.session.userId; const paramsUserId = req.params.userId; @@ -169,14 +161,14 @@ export function getUserOptionsHandler(dbPool: NodePgDatabase) { with: { question: true, }, - where: eq(db.options.userId, userId), + where: eq(schema.options.userId, userId), }); return res.json({ data: optionsQuery }); }; } -export function getUserRegistrationsHandler(dbPool: NodePgDatabase) { +export function getUserRegistrationsHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const userId = req.session.userId; const paramsUserId = req.params.userId; @@ -194,9 +186,9 @@ export function getUserRegistrationsHandler(dbPool: NodePgDatabase) { try { const query = await dbPool .select() - .from(db.registrations) - .leftJoin(db.events, eq(db.events.id, db.registrations.eventId)) - .where(eq(db.registrations.userId, userId)); + .from(schema.registrations) + .leftJoin(schema.events, eq(schema.events.id, schema.registrations.eventId)) + .where(eq(schema.registrations.userId, userId)); const out = query.map((q) => { return { diff --git a/src/handlers/votes.ts b/src/handlers/votes.ts index 41449fef..3cd7ab8c 100644 --- a/src/handlers/votes.ts +++ b/src/handlers/votes.ts @@ -1,17 +1,14 @@ import type { Request, Response } from 'express'; import { z } from 'zod'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { validateAndSaveVotes, updateOptionScore } from '../services/votes'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { logger } from '../utils/logger'; /** * Handler function that saves votes submitted by a user. - * @param { NodePgDatabase} dbPool - The database connection pool. - * @param {Request} req - The Express request object containing the user's submitted votes. - * @param {Response} res - The Express response object to send the result. */ -export function saveVotesHandler(dbPool: NodePgDatabase) { +export function saveVotesHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const userId = req.session.userId; diff --git a/src/index.ts b/src/index.ts index b1fb7bef..208a580b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,8 @@ import { default as express } from 'express'; import { apiRouter } from './routers/api'; import { environmentVariables } from './types'; -import { createDbPool } from './utils/db/create-db-connection'; -import { runMigrations } from './utils/db/run-migrations'; import { logger } from './utils/logger'; +import { createDbPool, runMigrations } from './db'; const app = express(); async function main() { diff --git a/src/middleware/is-logged-in.ts b/src/middleware/is-logged-in.ts index 167db8a3..7f7dfdcc 100644 --- a/src/middleware/is-logged-in.ts +++ b/src/middleware/is-logged-in.ts @@ -1,17 +1,17 @@ import type { NextFunction, Response, Request } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -export function isLoggedIn(dbPool: NodePgDatabase) { +export function isLoggedIn(dbPool: NodePgDatabase) { return async function (req: Request, res: Response, next: NextFunction) { if (req.session?.userId) { const rows = await dbPool .selectDistinct({ - id: db.users.id, + id: schema.users.id, }) - .from(db.users) - .where(eq(db.users.id, req.session.userId)); + .from(schema.users) + .where(eq(schema.users.id, req.session.userId)); if (!rows.length) { return res.status(401).send(); diff --git a/src/modules/funding-mechanism.spec.ts b/src/modules/funding-mechanism.spec.ts index 13497b43..c06271c0 100644 --- a/src/modules/funding-mechanism.spec.ts +++ b/src/modules/funding-mechanism.spec.ts @@ -1,4 +1,6 @@ +import assert from 'node:assert'; import { allocateFunding } from './funding-mechanism'; +import { describe, test } from 'node:test'; describe('test funding mechanism', () => { test('calculates the funding according to the mechanism', () => { @@ -28,7 +30,7 @@ describe('test funding mechanism', () => { }; const result = allocateFunding(availableFunding, maxFunding, getOptionData); - expect(result).toEqual(expectedResult); + assert.deepEqual(result, expectedResult); }); test('Does not allocate funding to the lowest plurality score if no funding is availabe anymore', () => { @@ -58,7 +60,7 @@ describe('test funding mechanism', () => { }; const result = allocateFunding(availableFunding, maxFunding, getOptionData); - expect(result).toEqual(expectedResult); + assert.deepEqual(result, expectedResult); }); test('Does still allocate funding even if nothing was allocated to someone with a higher score because of a too high budget', () => { @@ -88,7 +90,7 @@ describe('test funding mechanism', () => { }; const result = allocateFunding(availableFunding, maxFunding, getOptionData); - expect(result).toEqual(expectedResult); + assert.deepEqual(result, expectedResult); }); test('Excludes projects from funding who specify more than the maximum amount', () => { @@ -118,6 +120,6 @@ describe('test funding mechanism', () => { }; const result = allocateFunding(availableFunding, maxFunding, getOptionData); - expect(result).toEqual(expectedResult); + assert.deepEqual(result, expectedResult); }); }); diff --git a/src/modules/plural-voting.spec.ts b/src/modules/plural-voting.spec.ts index d020d438..fe8e1486 100644 --- a/src/modules/plural-voting.spec.ts +++ b/src/modules/plural-voting.spec.ts @@ -1,5 +1,6 @@ -import { logger } from '../utils/logger'; +import assert from 'node:assert'; import { PluralVoting } from './plural-voting'; +import { describe, test } from 'node:test'; // Define instance outside the tests const groups: Record = { @@ -21,7 +22,7 @@ const pluralVoting = new PluralVoting(groups, contributions); describe('createGroupMemberships', () => { test('creates group memberships correctly', () => { const result = pluralVoting.createGroupMemberships(groups); - expect(result).toEqual({ + assert.deepEqual(result, { user0: ['group0', 'group2'], user1: ['group0', 'group1'], user2: ['group1', 'group2'], @@ -41,7 +42,7 @@ describe('commonGroup', () => { }; const result = pluralVoting.commonGroup('user0', 'user1', groupMemberships); - expect(result).toBe(true); + assert.equal(result, true); }); test('should return false if participants do not share a common group', () => { @@ -53,7 +54,7 @@ describe('commonGroup', () => { }; const result = pluralVoting.commonGroup('user0', 'user3', groupMemberships); - expect(result).toBe(false); + assert.equal(result, false); }); }); @@ -66,7 +67,7 @@ describe('K function', () => { const contributions = { user0: 4, user1: 9 }; const result = pluralVoting.K(agent, otherGroup, groupMemberships, contributions); - expect(result).toEqual(4); + assert.equal(result, 4); }); test('should attenuate votes of agent if agent has a shared group membership with another member of the group even if agent is not in the group', () => { @@ -76,7 +77,7 @@ describe('K function', () => { const contributions = { user0: 4, user1: 9 }; const result = pluralVoting.K(agent, otherGroup, groupMemberships, contributions); - expect(result).toEqual(2); + assert.equal(result, 2); }); test('should attenuate votes of agent solely because agent is in the other group himself', () => { @@ -88,7 +89,7 @@ describe('K function', () => { const contributions = { user0: 4, user1: 9 }; const result = pluralVoting.K(agent, otherGroup, groupMemberships, contributions); - expect(result).toEqual(2); + assert.equal(result, 2); }); test('should attenuate votes of agent if both conditions above that lead to attenuation are satisfied', () => { @@ -98,7 +99,7 @@ describe('K function', () => { const contributions = { user0: 4, user1: 9 }; const result = pluralVoting.K(agent, otherGroup, groupMemberships, contributions); - expect(result).toEqual(2); + assert.equal(result, 2); }); }); @@ -108,7 +109,7 @@ describe('arraysEqual', () => { const array1 = ['user0', 'user1']; const array2 = ['user1', 'user0']; const result = pluralVoting.arraysEqual(array1, array2); - expect(result).toBe(true); + assert(result); }); }); @@ -123,7 +124,7 @@ describe('removeDuplicateGroups', () => { group4: ['user2', 'user1'], }; const result = pluralVoting.removeDuplicateGroups(groups); - expect(result).toEqual({ + assert.deepEqual(result, { group0: ['user0'], group1: ['user1'], group3: ['user1', 'user2'], @@ -139,7 +140,7 @@ describe('removeDuplicateGroups', () => { group4: ['user1', 'user2'], }; const result = pluralVoting.removeDuplicateGroups(groups); - expect(result).toEqual({ + assert.deepEqual(result, { group0: ['user0'], group1: ['user1'], group3: ['user1', 'user2'], @@ -151,7 +152,7 @@ describe('removeDuplicateGroups', () => { group0: ['user0'], }; const result = pluralVoting.removeDuplicateGroups(groups); - expect(result).toEqual({ + assert.deepEqual(result, { group0: ['user0'], }); }); @@ -167,7 +168,7 @@ describe('clusterMatch', () => { const expectedScore = 4; const result = pluralVoting.clusterMatch(groups, contributions); - expect(result).toEqual(expectedScore); + assert.equal(result, expectedScore); }); test('calculates plurality score even if only one group is available', () => { @@ -178,7 +179,7 @@ describe('clusterMatch', () => { const expectedScore = 3; const result = pluralVoting.clusterMatch(groups, contributions); - expect(result).toEqual(expectedScore); + assert.equal(result, expectedScore); }); test('that plural score equals quadratic score when a single participant has different group memberships', () => { @@ -189,7 +190,7 @@ describe('clusterMatch', () => { const expectedScore = 3; const result = pluralVoting.clusterMatch(groups, contributions); - expect(result).toEqual(expectedScore); + assert.equal(result, expectedScore); }); test('that the interaction terms get neglected when calculating the plural score if all groups contain the same members', () => { @@ -203,7 +204,7 @@ describe('clusterMatch', () => { const expectedScore = 4; const result = pluralVoting.clusterMatch(groups, contributions); - expect(result).toEqual(expectedScore); + assert.equal(result, expectedScore); }); test('that the interaction terms get neglected if all groups contain the same members but the order is scrambled', () => { @@ -217,7 +218,7 @@ describe('clusterMatch', () => { const expectedScore = 4; const result = pluralVoting.clusterMatch(groups, contributions); - expect(result).toEqual(expectedScore); + assert.equal(result, expectedScore); }); test('that duplicate groups are excluded from the score calculation', () => { @@ -232,7 +233,7 @@ describe('clusterMatch', () => { const expectedScore = 6; const result = pluralVoting.clusterMatch(groups, contributions); - expect(result).toEqual(expectedScore); + assert.equal(result, expectedScore); }); test('that the plurality score equals zero when everyone votes 0', () => { @@ -243,12 +244,11 @@ describe('clusterMatch', () => { const expectedScore = 0; const result = pluralVoting.clusterMatch(groups, contributions); - expect(result).toEqual(expectedScore); + assert.equal(result, expectedScore); }); test('calculates plurality score according to connection oriented cluster match', () => { const score = pluralVoting.pluralScoreCalculation(); - logger.debug('Plurality Score:', score); - expect(true).toBe(true); + assert(score); }); }); diff --git a/src/modules/quadratic-voting.spec.ts b/src/modules/quadratic-voting.spec.ts index a6e72ccd..941a8bf2 100644 --- a/src/modules/quadratic-voting.spec.ts +++ b/src/modules/quadratic-voting.spec.ts @@ -1,5 +1,6 @@ -import { logger } from '../utils/logger'; +import assert from 'assert'; import { quadraticVoting } from './quadratic-voting'; +import { describe, test } from 'node:test'; describe('quadraticVoting', () => { test('calculates quadratic votes for each agent and sum of quadratic votes', () => { @@ -21,8 +22,8 @@ describe('quadraticVoting', () => { const [resultQuadraticVotesDict, resultSumQuadraticVotes] = quadraticVoting(votes); // Verify that the result is as expected - expect(resultQuadraticVotesDict).toEqual(expectedQuadraticVotesDict); - expect(resultSumQuadraticVotes).toEqual(expectedSumQuadraticVotes); + assert.deepStrictEqual(resultQuadraticVotesDict, expectedQuadraticVotesDict); + assert.strictEqual(resultSumQuadraticVotes, expectedSumQuadraticVotes); }); test('', () => { @@ -33,9 +34,7 @@ describe('quadraticVoting', () => { user3: 16, }; const [result, sum] = quadraticVoting(votes); - - logger.debug('Quadratic Votes:', result); - logger.debug('Sum of Quadratic Votes:', sum); - expect(true).toBe(true); + assert.deepStrictEqual(result, { user1: 2, user2: 3, user3: 4 }); + assert.strictEqual(sum, 9); }); }); diff --git a/src/routers/alerts.ts b/src/routers/alerts.ts index bbbc0d48..955e25e0 100644 --- a/src/routers/alerts.ts +++ b/src/routers/alerts.ts @@ -1,11 +1,11 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { getActiveAlerts } from '../handlers/alerts'; import { isLoggedIn } from '../middleware/is-logged-in'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function alertsRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function alertsRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.get('/', isLoggedIn(dbPool), getActiveAlerts(dbPool)); return router; } diff --git a/src/routers/api.ts b/src/routers/api.ts index 892842f7..c6e08dd3 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -1,5 +1,5 @@ import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { default as express } from 'express'; import { ironSession } from 'iron-session/express'; import { authRouter } from './auth'; @@ -31,7 +31,7 @@ export function apiRouter({ dbPool, cookiePassword, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; cookiePassword: string; }) { const router = express.Router(); diff --git a/src/routers/auth.ts b/src/routers/auth.ts index eac3d8bf..e17dd6ac 100644 --- a/src/routers/auth.ts +++ b/src/routers/auth.ts @@ -1,10 +1,10 @@ -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { default as express } from 'express'; import { destroySessionHandler, verifyPCDHandler } from '../handlers/auth'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function authRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function authRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.post('/zupass/verify', verifyPCDHandler(dbPool)); router.post('/logout', destroySessionHandler()); return router; diff --git a/src/routers/comments.ts b/src/routers/comments.ts index b6418936..42757a41 100644 --- a/src/routers/comments.ts +++ b/src/routers/comments.ts @@ -1,5 +1,5 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { isLoggedIn } from '../middleware/is-logged-in'; import { deleteCommentHandler, @@ -11,7 +11,7 @@ import { import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function commentsRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function commentsRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.post('/', isLoggedIn(dbPool), saveCommentHandler(dbPool)); router.delete('/:commentId', isLoggedIn(dbPool), deleteCommentHandler(dbPool)); router.get('/:commentId/likes', isLoggedIn(dbPool), getCommentLikesHandler(dbPool)); diff --git a/src/routers/cycles.ts b/src/routers/cycles.ts index 79c84a14..b0aae729 100644 --- a/src/routers/cycles.ts +++ b/src/routers/cycles.ts @@ -1,12 +1,12 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { isLoggedIn } from '../middleware/is-logged-in'; import { getActiveCyclesHandler, getCycleHandler, getCycleVotesHandler } from '../handlers/cycles'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function cyclesRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function cyclesRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.get('/', isLoggedIn(dbPool), getActiveCyclesHandler(dbPool)); router.get('/:cycleId', isLoggedIn(dbPool), getCycleHandler(dbPool)); router.get('/:cycleId/votes', isLoggedIn(dbPool), getCycleVotesHandler(dbPool)); diff --git a/src/routers/events.ts b/src/routers/events.ts index 2153944e..797d470f 100644 --- a/src/routers/events.ts +++ b/src/routers/events.ts @@ -1,5 +1,5 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { isLoggedIn } from '../middleware/is-logged-in'; import { getEventCyclesHandler, @@ -12,7 +12,7 @@ import { import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function eventsRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function eventsRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.get('/', isLoggedIn(dbPool), getEventsHandler(dbPool)); router.get('/:eventId', isLoggedIn(dbPool), getEventHandler(dbPool)); router.get( diff --git a/src/routers/group-categories.ts b/src/routers/group-categories.ts index 595c504e..9e18075d 100644 --- a/src/routers/group-categories.ts +++ b/src/routers/group-categories.ts @@ -1,5 +1,5 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { isLoggedIn } from '../middleware/is-logged-in'; import { getGroupCategoriesGroupsHandler, @@ -10,7 +10,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function groupCategoriesRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function groupCategoriesRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.get('/', isLoggedIn(dbPool), getGroupCategoriesHandler(dbPool)); router.get('/:id', isLoggedIn(dbPool), getGroupCategoryHandler(dbPool)); router.get('/:id/groups', isLoggedIn(dbPool), getGroupCategoriesGroupsHandler(dbPool)); diff --git a/src/routers/groups.ts b/src/routers/groups.ts index 8a30b328..6b781832 100644 --- a/src/routers/groups.ts +++ b/src/routers/groups.ts @@ -1,5 +1,5 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { isLoggedIn } from '../middleware/is-logged-in'; import { createGroupHandler, @@ -9,7 +9,7 @@ import { import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function groupsRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function groupsRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.post('/', isLoggedIn(dbPool), createGroupHandler(dbPool)); router.get('/:id/registrations', isLoggedIn(dbPool), getGroupRegistrationsHandler(dbPool)); router.get('/:id/users-to-groups', isLoggedIn(dbPool), getGroupMembersHandler(dbPool)); diff --git a/src/routers/options.ts b/src/routers/options.ts index 393a9d88..55b37b99 100644 --- a/src/routers/options.ts +++ b/src/routers/options.ts @@ -1,5 +1,5 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { getOptionUsersHandler, getOptionCommentsHandler, @@ -12,7 +12,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function optionsRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function optionsRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.post('/', isLoggedIn(dbPool), saveOptionHandler(dbPool)); router.put('/:optionId', isLoggedIn(dbPool)), updateOptionHandler(dbPool); router.get('/:optionId', isLoggedIn(dbPool), getOptionHandler(dbPool)); diff --git a/src/routers/questions.ts b/src/routers/questions.ts index d52d5f46..9735fb46 100644 --- a/src/routers/questions.ts +++ b/src/routers/questions.ts @@ -1,5 +1,5 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { isLoggedIn } from '../middleware/is-logged-in'; import { getCalculateFundingHandler, @@ -9,7 +9,7 @@ import { import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function forumQuestionsRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function forumQuestionsRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.get('/:forumQuestionId/hearts', isLoggedIn(dbPool), getQuestionHeartsHandler(dbPool)); router.get( '/:forumQuestionId/statistics', diff --git a/src/routers/registrations.ts b/src/routers/registrations.ts index 637a0638..e3c7deba 100644 --- a/src/routers/registrations.ts +++ b/src/routers/registrations.ts @@ -1,5 +1,5 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { isLoggedIn } from '../middleware/is-logged-in'; import { getRegistrationDataHandler, @@ -10,7 +10,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function registrationsRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function registrationsRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.post('/', isLoggedIn(dbPool), saveRegistrationHandler(dbPool)); router.put('/:id', isLoggedIn(dbPool), updateRegistrationHandler(dbPool)); router.get('/:id/registration-data', isLoggedIn(dbPool), getRegistrationDataHandler(dbPool)); diff --git a/src/routers/users-to-groups.ts b/src/routers/users-to-groups.ts index 964084f9..51984955 100644 --- a/src/routers/users-to-groups.ts +++ b/src/routers/users-to-groups.ts @@ -1,5 +1,5 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { isLoggedIn } from '../middleware/is-logged-in'; import { joinGroupsHandler, @@ -10,7 +10,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function usersToGroupsRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function usersToGroupsRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.post('/', isLoggedIn(dbPool), joinGroupsHandler(dbPool)); router.put('/:id', isLoggedIn(dbPool), updateGroupsHandler(dbPool)); router.delete('/:id', isLoggedIn(dbPool), leaveGroupsHandler(dbPool)); diff --git a/src/routers/users.ts b/src/routers/users.ts index ad3fd574..3ed07539 100644 --- a/src/routers/users.ts +++ b/src/routers/users.ts @@ -1,5 +1,5 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { getUserAttributesHandler, getUsersToGroupsHandler, @@ -13,7 +13,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function usersRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function usersRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.get('/', isLoggedIn(dbPool), getUserHandler(dbPool)); router.put('/:userId', isLoggedIn(dbPool), updateUserHandler(dbPool)); router.get('/:userId/users-to-groups', isLoggedIn(dbPool), getUsersToGroupsHandler(dbPool)); diff --git a/src/routers/votes.ts b/src/routers/votes.ts index d00e7dab..4fe1c01a 100644 --- a/src/routers/votes.ts +++ b/src/routers/votes.ts @@ -1,12 +1,12 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { isLoggedIn } from '../middleware/is-logged-in'; import { saveVotesHandler } from '../handlers/votes'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; const router = express.Router(); -export function votesRouter({ dbPool }: { dbPool: NodePgDatabase }) { +export function votesRouter({ dbPool }: { dbPool: NodePgDatabase }) { router.post('/', isLoggedIn(dbPool), saveVotesHandler(dbPool)); return router; } diff --git a/src/services/auth.ts b/src/services/auth.ts index 0ee4f820..5958048e 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -1,23 +1,23 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { eq } from 'drizzle-orm'; import { logger } from '../utils/logger'; export async function createOrSignInPCD( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: { uuid: string; email: string }, -): Promise { +): Promise { // check if there is a federated credential with the same subject - const federatedCredential: db.FederatedCredential[] = await dbPool + const federatedCredential: schema.FederatedCredential[] = await dbPool .select() - .from(db.federatedCredentials) - .where(eq(db.federatedCredentials.subject, data.uuid)); + .from(schema.federatedCredentials) + .where(eq(schema.federatedCredentials.subject, data.uuid)); if (federatedCredential.length === 0) { // create user try { - const user: db.User[] = await dbPool - .insert(db.users) + const user: schema.User[] = await dbPool + .insert(schema.users) .values({ email: data.email, }) @@ -27,7 +27,7 @@ export async function createOrSignInPCD( throw new Error('Failed to create user'); } - await dbPool.insert(db.federatedCredentials).values({ + await dbPool.insert(schema.federatedCredentials).values({ userId: user[0]?.id, provider: 'zupass', subject: data.uuid, @@ -44,7 +44,7 @@ export async function createOrSignInPCD( throw new Error('expected federated credential to exist'); } const user = await dbPool.query.users.findFirst({ - where: eq(db.users.id, federatedCredential[0].userId), + where: eq(schema.users.id, federatedCredential[0].userId), }); if (!user) { diff --git a/src/services/comments.spec.ts b/src/services/comments.spec.ts index 5396aeb6..3d6c28f7 100644 --- a/src/services/comments.spec.ts +++ b/src/services/comments.spec.ts @@ -1,45 +1,29 @@ -import * as db from '../db'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; -import { environmentVariables, insertSimpleRegistrationSchema } from '../types'; -import { cleanup, seed } from '../utils/db/seed'; -import { z } from 'zod'; -import { getOptionUsers } from './comments'; import { eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; +import { assert } from 'node:console'; +import { after, before, describe, test } from 'node:test'; +import { z } from 'zod'; +import { createTestDatabase, seed } from '../db'; +import * as schema from '../db/schema'; +import { environmentVariables, insertSimpleRegistrationSchema } from '../types'; +import { getOptionUsers } from './comments'; describe('service: comments', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; + let dbPool: NodePgDatabase; let groupRegistrationData: z.infer; - let secretCategory: db.GroupCategory | undefined; - let questionOption: db.Option | undefined; - let secretGroup: db.Group[]; - let cycle: db.Cycle | undefined; - let user: db.User | undefined; - let otherUser: db.User | undefined; + let secretCategory: schema.GroupCategory | undefined; + let questionOption: schema.Option | undefined; + let secretGroup: schema.Group[]; + let cycle: schema.Cycle | undefined; + let user: schema.User | undefined; + let otherUser: schema.User | undefined; + let deleteTestDatabase: () => Promise; - beforeAll(async () => { + before(async () => { const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - dbPool = initDb.db; - dbConnection = initDb.client; + const { dbClient, teardown } = await createTestDatabase(envVariables); + dbPool = dbClient.db; + deleteTestDatabase = teardown; // seed const { users, questionOptions, cycles, groups, groupCategories } = await seed(dbPool); // Insert registration fields for the user @@ -48,7 +32,7 @@ describe('service: comments', () => { user = users[0]; otherUser = users[1]; cycle = cycles[0]; - secretGroup = groups.filter((group) => group !== undefined) as db.Group[]; + secretGroup = groups.filter((group) => group !== undefined) as schema.Group[]; const secretGroupId = secretGroup[4]?.id ?? ''; groupRegistrationData = { @@ -59,32 +43,35 @@ describe('service: comments', () => { }; // Insert group registration data - await dbPool.insert(db.registrations).values(groupRegistrationData); + await dbPool.insert(schema.registrations).values(groupRegistrationData); // get registration Id const registrationIds = await dbPool .select({ - registrationId: db.registrations.id, + registrationId: schema.registrations.id, }) - .from(db.registrations); + .from(schema.registrations); const registrationId = registrationIds[0]?.registrationId; // update question options await dbPool - .update(db.options) + .update(schema.options) .set({ registrationId: registrationId!, userId: user?.id ?? '' }) - .where(eq(db.options.id, questionOption!.id)); + .where(eq(schema.options.id, questionOption!.id)); // update secret group - await dbPool.update(db.groups).set({ secret: '12345' }).where(eq(db.groups.id, secretGroupId)); + await dbPool + .update(schema.groups) + .set({ secret: '12345' }) + .where(eq(schema.groups.id, secretGroupId)); // insert users to groups - await dbPool.insert(db.usersToGroups).values({ + await dbPool.insert(schema.usersToGroups).values({ userId: user?.id ?? '', groupId: secretGroupId, groupCategoryId: secretCategory!.id, }); - await dbPool.insert(db.usersToGroups).values({ + await dbPool.insert(schema.usersToGroups).values({ userId: otherUser?.id ?? '', groupId: secretGroupId, groupCategoryId: secretCategory!.id, @@ -96,17 +83,16 @@ describe('service: comments', () => { // Call getOptionAuthors with the required parameters const result = await getOptionUsers(optionId, dbPool); - expect(result).toBeDefined(); + assert(result !== null); }); test('should return null if optionId does not exist', async () => { const nonExistentOptionId = '00000000-0000-0000-0000-000000000000'; const result = await getOptionUsers(nonExistentOptionId, dbPool); - expect(result).toBeNull(); + assert(result === null); }); - afterAll(async () => { - await cleanup(dbPool); - await dbConnection.end(); + after(async () => { + await deleteTestDatabase(); }); }); diff --git a/src/services/comments.ts b/src/services/comments.ts index 8309b1c0..ae1a4fe8 100644 --- a/src/services/comments.ts +++ b/src/services/comments.ts @@ -1,26 +1,22 @@ import { eq, and, sql } from 'drizzle-orm'; import { insertCommentSchema } from '../types'; import { z } from 'zod'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { logger } from '../utils/logger'; /** * Inserts a new comment into the database. - * @param { NodePgDatabase} dbPool - The database pool connection. - * @param {z.infer} data - The comment data to insert. - * @param {string} userId - The ID of the user making the comment. - * @returns {Promise} - A promise that resolves with the inserted comment. * @throws {Error} - Throws an error if the insertion fails. */ export async function saveComment( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: z.infer, userId: string, ) { try { const newComment = await dbPool - .insert(db.comments) + .insert(schema.comments) .values({ userId: userId, optionId: data.optionId, @@ -36,22 +32,20 @@ export async function saveComment( /** * Deletes a comment from the database, along with associated likes if any. - * @param { NodePgDatabase} 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 async function deleteComment( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: { commentId: string; userId: string; }, -): Promise<{ errors?: string[]; data?: db.Comment }> { +): Promise<{ errors?: string[]; data?: schema.Comment }> { const { commentId, userId } = data; // 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)), + where: and(eq(schema.comments.id, commentId), eq(schema.comments.userId, userId)), }); if (!comment) { @@ -59,19 +53,19 @@ export async function deleteComment( } // Delete all likes associated with the deleted comment - await dbPool.delete(db.likes).where(eq(db.likes.commentId, commentId)); + await dbPool.delete(schema.likes).where(eq(schema.likes.commentId, commentId)); // Delete the comment const deletedComment = await dbPool - .delete(db.comments) - .where(eq(db.comments.id, commentId)) + .delete(schema.comments) + .where(eq(schema.comments.id, commentId)) .returning(); return { data: deletedComment[0] }; } export async function getOptionComments( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: { optionId: string; }, @@ -79,9 +73,9 @@ export async function getOptionComments( // Query comments const rows = await dbPool .select() - .from(db.comments) - .leftJoin(db.users, eq(db.comments.userId, db.users.id)) - .where(eq(db.comments.optionId, data.optionId)); + .from(schema.comments) + .leftJoin(schema.users, eq(schema.comments.userId, schema.users.id)) + .where(eq(schema.comments.optionId, data.optionId)); const commentsWithUserNames = rows.map((row) => { return { @@ -104,13 +98,10 @@ export async function getOptionComments( /** * Checks whether a user can comment based on their registration status. - * @param { NodePgDatabase} dbPool - The PostgreSQL database pool. - * @param {string} userId - The ID of the user attempting to comment. - * @param {string | undefined | null} optionId - The ID of the option for which the user is attempting to comment. * @returns {Promise} A promise that resolves to true if the user can comment, false otherwise. */ export async function userCanComment( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, userId: string, optionId: string | undefined | null, ) { @@ -121,10 +112,12 @@ export async function userCanComment( // check if user has an approved registration const res = await dbPool .selectDistinct({ - user: db.registrations.userId, + user: schema.registrations.userId, }) - .from(db.registrations) - .where(and(eq(db.registrations.userId, userId), eq(db.registrations.status, 'APPROVED'))); + .from(schema.registrations) + .where( + and(eq(schema.registrations.userId, userId), eq(schema.registrations.status, 'APPROVED')), + ); if (!res.length) { return false; @@ -150,14 +143,10 @@ type GetOptionUsersResponse = { /** * Executes a query to retrieve user data related to a question option from the database. - * - * @param {string} optionId - The ID of the question option for which author data is to be retrieved. - * @param { NodePgDatabase} dbPool - The PostgreSQL database pool instance. - * @returns {Promise} - A promise resolving to user data related to the question question or null if no data found. */ export async function getOptionUsers( optionId: string, - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, ): Promise { try { const queryUsers = await dbPool.execute<{ diff --git a/src/services/cycles.spec.ts b/src/services/cycles.spec.ts index 1575a44d..0cde91d7 100644 --- a/src/services/cycles.spec.ts +++ b/src/services/cycles.spec.ts @@ -1,41 +1,26 @@ -import { Client } from 'pg'; -import * as db from '../db'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; -import { cleanup, seed } from '../utils/db/seed'; -import { GetCycleById, getCycleVotes } from './cycles'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import assert from 'node:assert/strict'; +import { after, before, describe, test } from 'node:test'; +import { createTestDatabase, seed } from '../db'; +import * as schema from '../db/schema'; import { environmentVariables } from '../types'; +import { GetCycleById, getCycleVotes } from './cycles'; describe('service: cycles', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; - let cycle: db.Cycle | undefined; - let questionOption: db.Option | undefined; - let forumQuestion: db.Question | undefined; - let user: db.User | undefined; - let secondUser: db.User | undefined; + let dbPool: NodePgDatabase; + let cycle: schema.Cycle | undefined; + let questionOption: schema.Option | undefined; + let forumQuestion: schema.Question | undefined; + let user: schema.User | undefined; + let secondUser: schema.User | undefined; + let deleteTestDatabase: () => Promise; - beforeAll(async () => { + before(async () => { const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); + const { dbClient, teardown } = await createTestDatabase(envVariables); + dbPool = dbClient.db; + deleteTestDatabase = teardown; - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - dbPool = initDb.db; - dbConnection = initDb.client; // Seed the database const { cycles, questionOptions, forumQuestions, users } = await seed(dbPool); cycle = cycles[0]; @@ -47,30 +32,25 @@ describe('service: cycles', () => { test('should get cycle by id', async () => { const response = await GetCycleById(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); + assert.equal(response.id, cycle?.id); + assert.equal(response.status, cycle?.status); + assert(Array.isArray(response.forumQuestions)); + assert(response.forumQuestions[0]); + assert(Array.isArray(response.forumQuestions[0].questionOptions)); + assert.deepEqual(response.createdAt, cycle?.createdAt); + assert.deepEqual(response.updatedAt, cycle?.updatedAt); }); test('should get latest votes related to user', async function () { // create vote in db - await dbPool.insert(db.votes).values({ + await dbPool.insert(schema.votes).values({ numOfVotes: 2, optionId: questionOption!.id, questionId: forumQuestion!.id, userId: user!.id, }); // create second interaction with option - await dbPool.insert(db.votes).values({ + await dbPool.insert(schema.votes).values({ numOfVotes: 10, optionId: questionOption!.id, questionId: forumQuestion!.id, @@ -79,19 +59,19 @@ describe('service: cycles', () => { const votes = await getCycleVotes(dbPool, user!.id, cycle!.id); // expect the latest votes - expect(votes[0]?.numOfVotes).toBe(10); + assert.equal(votes[0]?.numOfVotes, 10); }); test('should not get votes for other user', async function () { - // create vote in db - await dbPool.insert(db.votes).values({ + // create vote in schema + await dbPool.insert(schema.votes).values({ numOfVotes: 2, optionId: questionOption!.id, questionId: forumQuestion!.id, userId: secondUser!.id, }); // create second interaction with option - await dbPool.insert(db.votes).values({ + await dbPool.insert(schema.votes).values({ numOfVotes: 10, optionId: questionOption!.id, questionId: forumQuestion!.id, @@ -102,11 +82,10 @@ describe('service: cycles', () => { const votes = await getCycleVotes(dbPool, user!.id, cycle!.id); // no votes have otherUser's id in array - expect(votes.filter((vote) => vote.userId === secondUser?.id).length).toBe(0); + assert.equal(votes.filter((vote) => vote.userId === secondUser?.id).length, 0); }); - afterAll(async () => { - await cleanup(dbPool); - await dbConnection.end(); + after(async () => { + await deleteTestDatabase(); }); }); diff --git a/src/services/cycles.ts b/src/services/cycles.ts index a4d98707..5d3e6091 100644 --- a/src/services/cycles.ts +++ b/src/services/cycles.ts @@ -1,10 +1,10 @@ import { eq, sql } from 'drizzle-orm'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -export async function GetCycleById(dbPool: NodePgDatabase, cycleId: string) { +export async function GetCycleById(dbPool: NodePgDatabase, cycleId: string) { const cycle = await dbPool.query.cycles.findFirst({ - where: eq(db.cycles.id, cycleId), + where: eq(schema.cycles.id, cycleId), with: { questions: { with: { @@ -27,7 +27,7 @@ export async function GetCycleById(dbPool: NodePgDatabase, cycleId: s }, }, }, - where: eq(db.options.show, true), + where: eq(schema.options.show, true), }, }, }, @@ -73,7 +73,7 @@ export async function GetCycleById(dbPool: NodePgDatabase, cycleId: s * @param {string} cycleId - The ID of the cycle. */ export async function getCycleVotes( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, userId: string, cycleId: string, ) { @@ -88,7 +88,7 @@ export async function getCycleVotes( with: { votes: { where: ({ optionId }) => - sql`${db.votes.createdAt} = ( + sql`${schema.votes.createdAt} = ( SELECT MAX(created_at) FROM ( SELECT created_at, user_id FROM votes WHERE user_id = ${userId} AND option_id = ${optionId} @@ -100,7 +100,7 @@ export async function getCycleVotes( }, }, }, - where: eq(db.cycles.id, cycleId), + where: eq(schema.cycles.id, cycleId), }); const out = response.flatMap((cycle) => diff --git a/src/services/funding-mechanism.spec.ts b/src/services/funding-mechanism.spec.ts index ee8ddf32..098e5670 100644 --- a/src/services/funding-mechanism.spec.ts +++ b/src/services/funding-mechanism.spec.ts @@ -1,57 +1,40 @@ -import * as db from '../db'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import assert from 'node:assert/strict'; +import { after, before, describe, test } from 'node:test'; +import { createTestDatabase, seed } from '../db'; +import * as schema from '../db/schema'; import { environmentVariables } from '../types'; -import { cleanup, seed } from '../utils/db/seed'; import { calculateFunding } from './funding-mechanism'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; describe('service: funding-mechanism', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; - let question: db.Question; + let dbPool: NodePgDatabase; + let question: schema.Question; + let deleteTestDatabase: () => Promise; - beforeAll(async () => { + before(async () => { const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - dbPool = initDb.db; - dbConnection = initDb.client; + const { dbClient, teardown } = await createTestDatabase(envVariables); + dbPool = dbClient.db; + deleteTestDatabase = teardown; const { forumQuestions } = await seed(dbPool); question = forumQuestions[0]!; }); test('calculateFunding returns and error if the query returns no optionData', async () => { const response = await calculateFunding(dbPool, '00000000-0000-0000-0000-000000000000'); - expect(response.allocatedFunding).toBeNull(); - expect(response.remainingFunding).toBeNull(); - expect(response.error).toEqual(expect.any(String)); + assert.equal(response.allocatedFunding, null); + assert.equal(response.remainingFunding, null); + assert(response.error); }); test('calculateFunding returns the correct funding amount', async () => { const response = await calculateFunding(dbPool, question?.id); - expect(response.allocatedFunding).toBeDefined(); - expect(response.remainingFunding).toEqual(100000); - expect(response.error).toBeNull(); + assert(response.allocatedFunding); + assert.equal(response.remainingFunding, 100000); + assert.equal(response.error, null); }); - afterAll(async () => { - await cleanup(dbPool); - await dbConnection.end(); + after(async () => { + await deleteTestDatabase(); }); }); diff --git a/src/services/funding-mechanism.ts b/src/services/funding-mechanism.ts index 603e25fb..96a7a484 100644 --- a/src/services/funding-mechanism.ts +++ b/src/services/funding-mechanism.ts @@ -1,5 +1,5 @@ import { eq } from 'drizzle-orm'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { allocateFunding } from '../modules/funding-mechanism'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; @@ -12,7 +12,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; * - A promise resolving to an object containing the allocated funding for each project and the remaining funding. */ export async function calculateFunding( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, forumQuestionId: string, ): Promise<{ allocatedFunding: { [key: string]: number } | null; @@ -21,12 +21,12 @@ export async function calculateFunding( }> { const getOptionData = await dbPool .select({ - id: db.options.id, - voteScore: db.options.voteScore, - fundingRequest: db.options.fundingRequest, + id: schema.options.id, + voteScore: schema.options.voteScore, + fundingRequest: schema.options.fundingRequest, }) - .from(db.options) - .where(eq(db.options.questionId, forumQuestionId)); + .from(schema.options) + .where(eq(schema.options.questionId, forumQuestionId)); if (getOptionData.length === 0) { return { diff --git a/src/services/group-categories.spec.ts b/src/services/group-categories.spec.ts index b324af6b..f8739880 100644 --- a/src/services/group-categories.spec.ts +++ b/src/services/group-categories.spec.ts @@ -1,38 +1,22 @@ -import * as db from '../db'; -import { environmentVariables } from '../types'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; -import { cleanup, seed } from '../utils/db/seed'; -import { canCreateGroupInGroupCategory, canViewGroupsInGroupCategory } from './group-categories'; import { eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; +import assert from 'node:assert/strict'; +import { after, before, describe, test } from 'node:test'; +import { createTestDatabase, seed } from '../db'; +import * as schema from '../db/schema'; +import { environmentVariables } from '../types'; +import { canCreateGroupInGroupCategory, canViewGroupsInGroupCategory } from './group-categories'; describe('service: groupCategories', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; - let groupCategory: db.GroupCategory | undefined; + let dbPool: NodePgDatabase; + let groupCategory: schema.GroupCategory | undefined; + let deleteTestDatabase: () => Promise; - beforeAll(async () => { + before(async () => { const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - dbPool = initDb.db; - dbConnection = initDb.client; + const { dbClient, teardown } = await createTestDatabase(envVariables); + dbPool = dbClient.db; + deleteTestDatabase = teardown; // seed const { groupCategories } = await seed(dbPool); @@ -47,7 +31,7 @@ describe('service: groupCategories', () => { const canCreate = await canCreateGroupInGroupCategory(dbPool, groupCategory.id); - expect(canCreate).toBe(false); + assert.equal(canCreate, false); }); test('userCanCreate: true', async function () { @@ -56,13 +40,13 @@ describe('service: groupCategories', () => { } await dbPool - .update(db.groupCategories) + .update(schema.groupCategories) .set({ userCanCreate: true }) - .where(eq(db.groupCategories.id, groupCategory.id)); + .where(eq(schema.groupCategories.id, groupCategory.id)); const canCreate = await canCreateGroupInGroupCategory(dbPool, groupCategory.id); - expect(canCreate).toBe(true); + assert.equal(canCreate, true); }); }); @@ -73,13 +57,13 @@ describe('service: groupCategories', () => { } await dbPool - .update(db.groupCategories) + .update(schema.groupCategories) .set({ userCanView: false }) - .where(eq(db.groupCategories.id, groupCategory.id)); + .where(eq(schema.groupCategories.id, groupCategory.id)); const canView = await canViewGroupsInGroupCategory(dbPool, groupCategory.id); - expect(canView).toBe(false); + assert.equal(canView, false); }); test('userCanView: true', async function () { if (!groupCategory) { @@ -87,18 +71,17 @@ describe('service: groupCategories', () => { } await dbPool - .update(db.groupCategories) + .update(schema.groupCategories) .set({ userCanView: true }) - .where(eq(db.groupCategories.id, groupCategory.id)); + .where(eq(schema.groupCategories.id, groupCategory.id)); const canView = await canViewGroupsInGroupCategory(dbPool, groupCategory.id); - expect(canView).toBe(true); + assert.equal(canView, true); }); }); - afterAll(async () => { - await cleanup(dbPool); - await dbConnection.end(); + after(async () => { + await deleteTestDatabase(); }); }); diff --git a/src/services/group-categories.ts b/src/services/group-categories.ts index e1bcbb7e..2317587d 100644 --- a/src/services/group-categories.ts +++ b/src/services/group-categories.ts @@ -1,13 +1,13 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { eq } from 'drizzle-orm'; export async function canCreateGroupInGroupCategory( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, groupCategoryId: string, ) { const groupCategory = await dbPool.query.groupCategories.findFirst({ - where: eq(db.groupCategories.id, groupCategoryId), + where: eq(schema.groupCategories.id, groupCategoryId), }); if (!groupCategory) { @@ -18,11 +18,11 @@ export async function canCreateGroupInGroupCategory( } export async function canViewGroupsInGroupCategory( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, groupCategoryId: string, ) { const groupCategory = await dbPool.query.groupCategories.findFirst({ - where: eq(db.groupCategories.id, groupCategoryId), + where: eq(schema.groupCategories.id, groupCategoryId), }); if (!groupCategory) { diff --git a/src/services/groups.spec.ts b/src/services/groups.spec.ts index 611479a5..f3f15523 100644 --- a/src/services/groups.spec.ts +++ b/src/services/groups.spec.ts @@ -1,19 +1,18 @@ -import * as db from '../db'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; -import { cleanup, seed } from '../utils/db/seed'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import assert from 'node:assert/strict'; +import { after, before, describe, test } from 'node:test'; +import { z } from 'zod'; +import { createTestDatabase, seed } from '../db'; +import * as schema from '../db/schema'; +import { environmentVariables, insertSimpleRegistrationSchema } from '../types'; import { createSecretGroup, generateSecret, - getSecretGroup, getGroupMembers, getGroupRegistrations, + getSecretGroup, isUserIsPartOfGroup, } from './groups'; -import { environmentVariables, insertSimpleRegistrationSchema } from '../types'; -import { z } from 'zod'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; // Define sample wordlist to test the secret generator const wordlist: string[] = [ @@ -46,42 +45,27 @@ const wordlist: string[] = [ ]; describe('service: groups', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; - let group: db.Group[]; + let dbPool: NodePgDatabase; + let group: schema.Group[]; let groupRegistrationData: z.infer; - let secretGroup: db.Group[]; - let cycle: db.Cycle | undefined; - let user: db.User | undefined; - let groupCategory: db.GroupCategory | undefined; + let secretGroup: schema.Group[]; + let cycle: schema.Cycle | undefined; + let user: schema.User | undefined; + let groupCategory: schema.GroupCategory | undefined; + let deleteTestDatabase: () => Promise; - beforeAll(async () => { + before(async () => { const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - dbPool = initDb.db; - dbConnection = initDb.client; + const { dbClient, teardown } = await createTestDatabase(envVariables); + dbPool = dbClient.db; + deleteTestDatabase = teardown; const { users, cycles, groups, groupCategories } = await seed(dbPool); - group = groups.filter((group) => group !== undefined) as db.Group[]; + group = groups.filter((group) => group !== undefined) as schema.Group[]; user = users[0]; cycle = cycles[0]; groupCategory = groupCategories[0]; - secretGroup = groups.filter((group) => group !== undefined) as db.Group[]; + secretGroup = groups.filter((group) => group !== undefined) as schema.Group[]; const secretGroupId = secretGroup[4]?.id ?? ''; groupRegistrationData = { @@ -92,22 +76,20 @@ describe('service: groups', () => { }; // Insert group registration data - await dbPool.insert(db.registrations).values(groupRegistrationData); + await dbPool.insert(schema.registrations).values(groupRegistrationData); }); test('generate secret:', async function () { const secret = generateSecret(wordlist, 3); const words = secret.split('-'); - expect(words).toHaveLength(3); + assert.equal(words.length, 3); }); test('generate multiple secrets:', async function () { const secrets = Array.from({ length: 10 }, () => generateSecret(wordlist, 3)); - expect(secrets).toHaveLength(10); - expect(secrets).toEqual(expect.arrayContaining(secrets)); - // none should be the same - expect(new Set(secrets).size).toBe(secrets.length); + assert.equal(secrets.length, 10); + assert.equal(secrets.length, new Set(secrets).size); }); test('create a group:', async function () { @@ -117,13 +99,13 @@ describe('service: groups', () => { groupCategoryId: groupCategory!.id, }); - expect(rows).toHaveLength(1); - expect(rows[0]?.name).toBe('Test Group'); - expect(rows[0]?.description).toBe('Test Description'); + assert.equal(rows.length, 1); + assert.equal(rows[0]?.name, 'Test Group'); + assert.equal(rows[0]?.description, 'Test Description'); // secret should be generated const secret = rows[0]?.secret; const words = secret!.split('-'); - expect(words).toHaveLength(3); + assert.equal(words.length, 3); }); test('get a group:', async function () { @@ -135,26 +117,27 @@ describe('service: groups', () => { const group = await getSecretGroup(dbPool, rows[0]?.secret ?? ''); - expect(group?.name).toBe('Test Group'); - expect(group?.description).toBe('Test Description'); + assert.equal(group?.id, rows[0]?.id); + assert.equal(group?.name, 'Test Group'); + assert.equal(group?.description, 'Test Description'); }); test('get group members of a group', async () => { const groupId = group[1]?.id ?? ''; const result = await getGroupMembers(dbPool, groupId); - expect(result).toBeDefined(); + assert(result); }); test('get group registrations', async () => { const groupId = group[4]?.id ?? ''; const result = await getGroupRegistrations(dbPool, groupId); - expect(result).toBeDefined(); + assert(result); }); describe('authorization', function () { test('when the user is not in the group', async function () { const rows = await dbPool - .insert(db.groups) + .insert(schema.groups) .values({ groupCategoryId: groupCategory!.id, name: 'Test Group', @@ -175,11 +158,11 @@ describe('service: groups', () => { groupId: rows[0].id, }); - expect(result).toBe(false); + assert.equal(result, false); }); test('when the user is in the group', async function () { const rows = await dbPool - .insert(db.groups) + .insert(schema.groups) .values({ groupCategoryId: groupCategory!.id, name: 'Test Group', @@ -195,7 +178,7 @@ describe('service: groups', () => { } const userGroup = await dbPool - .insert(db.usersToGroups) + .insert(schema.usersToGroups) .values({ userId: user!.id, groupId: rows[0].id, @@ -212,12 +195,11 @@ describe('service: groups', () => { groupId: rows[0].id, }); - expect(result).toBe(true); + assert.equal(result, true); }); }); - afterAll(async () => { - await cleanup(dbPool); - await dbConnection.end(); + after(async () => { + await deleteTestDatabase(); }); }); diff --git a/src/services/groups.ts b/src/services/groups.ts index 0f93de3d..e400beea 100644 --- a/src/services/groups.ts +++ b/src/services/groups.ts @@ -1,18 +1,18 @@ import { and, eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { z } from 'zod'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { insertGroupsSchema } from '../types/groups'; -import { wordlist } from '../utils/db/mnemonics'; +import { wordlist } from '../utils/mnemonics'; export function createSecretGroup( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, body: z.infer, ) { const secret = generateSecret(wordlist, 3); const rows = dbPool - .insert(db.groups) + .insert(schema.groups) .values({ ...body, secret, @@ -22,9 +22,9 @@ export function createSecretGroup( return rows; } -export function getSecretGroup(dbPool: NodePgDatabase, secret: string) { +export function getSecretGroup(dbPool: NodePgDatabase, secret: string) { const group = dbPool.query.groups.findFirst({ - where: eq(db.groups.secret, secret), + where: eq(schema.groups.secret, secret), }); return group; @@ -43,13 +43,10 @@ export function generateSecret(wordlist: string[], length: number): string { /** * Executes a query to retrieve the members of a group. - - * @param { NodePgDatabase} dbPool - The database connection pool. - * @param {string} groupId - The ID of the user. */ -export async function getGroupMembers(dbPool: NodePgDatabase, groupId: string) { +export async function getGroupMembers(dbPool: NodePgDatabase, groupId: string) { const response = await dbPool.query.groups.findMany({ - where: eq(db.groups.id, groupId), + where: eq(schema.groups.id, groupId), with: { usersToGroups: { with: { @@ -73,13 +70,13 @@ export async function getGroupMembers(dbPool: NodePgDatabase, groupId /** * Executes a query to retrieve the registrations of a group. - - * @param { NodePgDatabase} dbPool - The database connection pool. - * @param {string} groupId - The ID of the user. */ -export async function getGroupRegistrations(dbPool: NodePgDatabase, groupId: string) { +export async function getGroupRegistrations( + dbPool: NodePgDatabase, + groupId: string, +) { const response = await dbPool.query.groups.findMany({ - where: eq(db.groups.id, groupId), + where: eq(schema.groups.id, groupId), columns: { secret: false, }, @@ -104,13 +101,16 @@ export async function isUserIsPartOfGroup({ userId, groupId, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; userId: string; groupId?: string | null; }) { if (groupId) { const userGroup = await dbPool.query.usersToGroups.findFirst({ - where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, groupId)), + where: and( + eq(schema.usersToGroups.userId, userId), + eq(schema.usersToGroups.groupId, groupId), + ), }); if (!userGroup) { diff --git a/src/services/likes.ts b/src/services/likes.ts index 9181d3dc..dd2580ca 100644 --- a/src/services/likes.ts +++ b/src/services/likes.ts @@ -1,21 +1,21 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { and, eq } from 'drizzle-orm'; export async function saveCommentLike( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: { commentId: string; userId: string; }, ): Promise<{ - data?: db.Like; + data?: schema.Like; errors?: string[]; }> { const { commentId, userId } = data; const like = await dbPool.query.likes.findFirst({ - where: and(eq(db.likes.commentId, commentId), eq(db.likes.userId, userId)), + where: and(eq(schema.likes.commentId, commentId), eq(schema.likes.userId, userId)), }); if (like) { @@ -24,7 +24,7 @@ export async function saveCommentLike( try { const newLike = await dbPool - .insert(db.likes) + .insert(schema.likes) .values({ commentId, userId, @@ -38,19 +38,19 @@ export async function saveCommentLike( } export async function deleteCommentLike( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: { commentId: string; userId: string; }, ): Promise<{ - data?: db.Like; + data?: schema.Like; errors?: string[]; }> { const { commentId, userId } = data; const like = await dbPool.query.likes.findFirst({ - where: and(eq(db.likes.commentId, commentId), eq(db.likes.userId, userId)), + where: and(eq(schema.likes.commentId, commentId), eq(schema.likes.userId, userId)), }); if (!like) { @@ -58,7 +58,10 @@ export async function deleteCommentLike( } try { - const deletedLike = await dbPool.delete(db.likes).where(eq(db.likes.id, like.id)).returning(); + const deletedLike = await dbPool + .delete(schema.likes) + .where(eq(schema.likes.id, like.id)) + .returning(); return { data: deletedLike[0] }; } catch (e) { return { errors: ['Failed to delete like'] }; @@ -67,13 +70,9 @@ export async function deleteCommentLike( /** * Checks whether a user can like a comment based on their registration status. - * @param { NodePgDatabase} dbPool - The PostgreSQL database pool. - * @param {string} userId - The ID of the user attempting to like the comment. - * @param {string} commentId - The ID of the comment to be liked. - * @returns {Promise} A promise that resolves to true if the user can like the comment, false otherwise. */ export async function userCanLike( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, userId: string, commentId: string, ) { @@ -84,10 +83,12 @@ export async function userCanLike( // check if user has an approved registration const res = await dbPool .selectDistinct({ - user: db.registrations.userId, + user: schema.registrations.userId, }) - .from(db.registrations) - .where(and(eq(db.registrations.userId, userId), eq(db.registrations.status, 'APPROVED'))); + .from(schema.registrations) + .where( + and(eq(schema.registrations.userId, userId), eq(schema.registrations.status, 'APPROVED')), + ); if (!res.length) { return false; diff --git a/src/services/options.ts b/src/services/options.ts index 5301454b..582d45b4 100644 --- a/src/services/options.ts +++ b/src/services/options.ts @@ -1,7 +1,7 @@ import { and, eq } from 'drizzle-orm'; import { z } from 'zod'; import { fieldsSchema, insertOptionsSchema } from '../types'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { enforceRules } from './validation'; @@ -10,12 +10,12 @@ export async function getUserOption({ optionId, userId, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; userId: string; optionId: string; -}): Promise { +}): Promise { const existingOption = await dbPool.query.options.findFirst({ - where: and(eq(db.options.userId, userId), eq(db.options.id, optionId)), + where: and(eq(schema.options.userId, userId), eq(schema.options.id, optionId)), }); if (!existingOption) { @@ -26,7 +26,7 @@ export async function getUserOption({ } export async function saveOption( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: z.infer, ) { const newOption = await createOptionInDB(dbPool, { @@ -45,9 +45,9 @@ export async function updateOption({ dbPool, option, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; data: z.infer; - option: db.Option; + option: schema.Option; }) { const updatedRegistration = await updateOptionInDB(dbPool, option, data); @@ -63,11 +63,11 @@ export async function updateOption({ } async function createOptionInDB( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, body: z.infer, ) { const rows = await dbPool - .insert(db.options) + .insert(schema.options) .values({ userId: body.userId, questionId: body.questionId, @@ -81,12 +81,12 @@ async function createOptionInDB( } async function updateOptionInDB( - dbPool: NodePgDatabase, - option: db.Option, + dbPool: NodePgDatabase, + option: schema.Option, body: z.infer, ) { const rows = await dbPool - .update(db.options) + .update(schema.options) .set({ groupId: body.groupId, questionId: body.questionId, @@ -95,7 +95,7 @@ async function updateOptionInDB( subTitle: body.subTitle, updatedAt: new Date(), }) - .where(and(eq(db.options.id, option.id))) + .where(and(eq(schema.options.id, option.id))) .returning(); return rows[0]; } @@ -104,13 +104,13 @@ export async function validateOptionData({ option, dbPool, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; option: z.infer; }) { const rows = await dbPool .select() - .from(db.questions) - .where(eq(db.questions.id, option.questionId)); + .from(schema.questions) + .where(eq(schema.questions.id, option.questionId)); if (!rows.length) { return []; @@ -139,13 +139,13 @@ export async function canUserCreateOption({ option, dbPool, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; option: z.infer; }): Promise { const rows = await dbPool .select() - .from(db.questions) - .where(eq(db.questions.id, option.questionId)); + .from(schema.questions) + .where(eq(schema.questions.id, option.questionId)); if (!rows.length) { return false; diff --git a/src/services/questions.spec.ts b/src/services/questions.spec.ts index 2eef8770..3d65ade5 100644 --- a/src/services/questions.spec.ts +++ b/src/services/questions.spec.ts @@ -1,4 +1,6 @@ import { availableHearts } from './questions'; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; // Test availableHearts function describe('service: forumQuestions', () => { @@ -9,7 +11,7 @@ describe('service: forumQuestions', () => { const maxRatio = 0.8; const result = availableHearts(numProposals, baseNumerator, baseDenominator, maxRatio); - expect(result).toEqual(5); + assert.equal(result, 5); }); test('error if max ratio was not calculated correctly', () => { @@ -19,7 +21,7 @@ describe('service: forumQuestions', () => { const maxRatio = 0.9; const result = availableHearts(numProposals, baseNumerator, baseDenominator, maxRatio); - expect(result).toEqual(0); + assert.equal(result, 0); }); test('returns custom hearts if customHearts is set', () => { @@ -36,7 +38,7 @@ describe('service: forumQuestions', () => { maxRatio, customHearts, ); - expect(result).toEqual(customHearts); + assert.equal(result, customHearts); }); test('executes the function if customHearts is not set', () => { @@ -46,7 +48,7 @@ describe('service: forumQuestions', () => { const maxRatio = 0.8; const result = availableHearts(numProposals, baseNumerator, baseDenominator, maxRatio); - expect(result).toEqual(5); + assert.equal(result, 5); }); test('executes the function if customHearts is set to less than 2', () => { @@ -63,7 +65,7 @@ describe('service: forumQuestions', () => { maxRatio, customHearts, ); - expect(result).toEqual(5); + assert.equal(result, 5); }); test('that function returns 0 in case the number of proposals are less than 2', () => { @@ -73,6 +75,6 @@ describe('service: forumQuestions', () => { const maxRatio = 0.8; const result = availableHearts(numProposals, baseNumerator, baseDenominator, maxRatio); - expect(result).toEqual(0); + assert.equal(result, 0); }); }); diff --git a/src/services/questions.ts b/src/services/questions.ts index 368d072d..025d87f8 100644 --- a/src/services/questions.ts +++ b/src/services/questions.ts @@ -1,5 +1,5 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { eq, sql } from 'drizzle-orm'; import { z } from 'zod'; import { insertOptionsSchema } from '../types/options'; @@ -7,6 +7,10 @@ import { fieldsSchema } from '../types'; import { enforceRules } from './validation'; import { logger } from '../utils/logger'; +/** + * Calculates number of hearts that a participant has available. The underlying assumption of the calculation is + that a participant must assign at least one heart to each available proposal. + */ export function availableHearts( numProposals: number, baseNumerator: number, @@ -14,15 +18,6 @@ export function availableHearts( maxRatio: number, customHearts: number | null = null, ): number | null { - // Calculates number of hearts that a participant has available. The underlying assumption of the calculation is - // that a participant must assign at least one heart to each available proposal. - // :param: numProposals: number of proposals (options) that can be votes on. - // :param: baseNumerator: specifies the minimum amounts of hearts a participant must allocate to a given proposal to satisfy the max ratio. - // :param: baseDenominator: specifies the minimum amount of hearts a participant must have available to satisfy the max ratio. - // :param: maxRatio: specifies the preference ratio a participant should be able to express over two project options. - // :param: customHearts: if this parameter is set then the function will return custom hearts independent of the number of projects. - // :returns: number of a available heart for each participant given a number of proposals. - if (customHearts !== null && customHearts >= 2) { return customHearts; } @@ -44,7 +39,7 @@ export function availableHearts( } export async function getQuestionHearts( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: { forumQuestionId: string; }, @@ -81,13 +76,13 @@ export async function validateQuestionFields({ option, dbPool, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; option: z.infer; }) { const rows = await dbPool .select() - .from(db.questions) - .where(eq(db.questions.id, option.questionId)); + .from(schema.questions) + .where(eq(schema.questions.id, option.questionId)); if (!rows.length) { return []; diff --git a/src/services/registrations.ts b/src/services/registrations.ts index 3b2265f3..4de8a04b 100644 --- a/src/services/registrations.ts +++ b/src/services/registrations.ts @@ -1,7 +1,7 @@ import { and, eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { z } from 'zod'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { fieldsSchema, insertRegistrationSchema } from '../types'; import { enforceRules } from './validation'; @@ -10,12 +10,15 @@ export async function getUserRegistration({ registrationId, userId, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; userId: string; registrationId: string; -}): Promise { +}): Promise { const existingRegistration = await dbPool.query.registrations.findFirst({ - where: and(eq(db.registrations.userId, userId), eq(db.registrations.id, registrationId)), + where: and( + eq(schema.registrations.userId, userId), + eq(schema.registrations.id, registrationId), + ), }); if (!existingRegistration) { @@ -29,10 +32,13 @@ export async function validateEventFields({ registration, dbPool, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; registration: z.infer; }) { - const rows = await dbPool.select().from(db.events).where(eq(db.events.id, registration.eventId)); + const rows = await dbPool + .select() + .from(schema.events) + .where(eq(schema.events.id, registration.eventId)); if (!rows.length) { return []; @@ -58,11 +64,11 @@ export async function validateEventFields({ } export async function saveRegistration( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, registration: z.infer, ) { const event = await dbPool.query.events.findFirst({ - where: eq(db.events.id, registration.eventId), + where: eq(schema.events.id, registration.eventId), }); if (!event) { @@ -90,9 +96,9 @@ export async function updateRegistration({ dbPool, registration, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; data: z.infer; - registration: db.Registration; + registration: schema.Registration; }) { const updatedRegistration = await updateRegistrationInDB(dbPool, registration, data); @@ -108,12 +114,12 @@ export async function updateRegistration({ } async function createRegistrationInDB( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, body: z.infer, ) { // insert to registration table const newRegistration = await dbPool - .insert(db.registrations) + .insert(schema.registrations) .values({ userId: body.userId, groupId: body.groupId, @@ -126,19 +132,19 @@ async function createRegistrationInDB( } async function updateRegistrationInDB( - dbPool: NodePgDatabase, - registration: db.Registration, + dbPool: NodePgDatabase, + registration: schema.Registration, body: z.infer, ) { const updatedRegistration = await dbPool - .update(db.registrations) + .update(schema.registrations) .set({ eventId: body.eventId, groupId: body.groupId, data: body.data, updatedAt: new Date(), }) - .where(eq(db.registrations.id, registration.id)) + .where(eq(schema.registrations.id, registration.id)) .returning(); return updatedRegistration[0]; } @@ -147,10 +153,13 @@ export async function validateRegistrationData({ registration, dbPool, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; registration: z.infer; }) { - const rows = await dbPool.select().from(db.events).where(eq(db.events.id, registration.eventId)); + const rows = await dbPool + .select() + .from(schema.events) + .where(eq(schema.events.id, registration.eventId)); if (!rows.length) { return []; diff --git a/src/services/statistics.spec.ts b/src/services/statistics.spec.ts index 57eefa62..8afb7d4c 100644 --- a/src/services/statistics.spec.ts +++ b/src/services/statistics.spec.ts @@ -1,43 +1,28 @@ -import * as db from '../db'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; -import { environmentVariables, insertVotesSchema } from '../types'; -import { cleanup, seed } from '../utils/db/seed'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import assert from 'node:assert/strict'; +import { after, before, describe, test } from 'node:test'; import { z } from 'zod'; +import { createTestDatabase, seed } from '../db'; +import * as schema from '../db/schema'; +import { environmentVariables, insertVotesSchema } from '../types'; import { executeResultQueries } from './statistics'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; describe('service: statistics', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; + let dbPool: NodePgDatabase; let userTestData: z.infer; let otherUserTestData: z.infer; - let questionOption: db.Option | undefined; - let forumQuestion: db.Question | undefined; - let user: db.User | undefined; - let otherUser: db.User | undefined; + let questionOption: schema.Option | undefined; + let forumQuestion: schema.Question | undefined; + let user: schema.User | undefined; + let otherUser: schema.User | undefined; + let deleteTestDatabase: () => Promise; - beforeAll(async () => { + before(async () => { const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); + const { dbClient, teardown } = await createTestDatabase(envVariables); + dbPool = dbClient.db; + deleteTestDatabase = teardown; - dbPool = initDb.db; - dbConnection = initDb.client; // seed const { users, questionOptions, forumQuestions } = await seed(dbPool); // Insert registration fields for the user @@ -59,8 +44,8 @@ describe('service: statistics', () => { }; // Add additional data to the Db - await dbPool.insert(db.votes).values(userTestData); - await dbPool.insert(db.votes).values(otherUserTestData); + await dbPool.insert(schema.votes).values(userTestData); + await dbPool.insert(schema.votes).values(otherUserTestData); }); test('should return aggregated statistics when all queries return valid data', async () => { @@ -70,45 +55,37 @@ describe('service: statistics', () => { const result = await executeResultQueries(questionId, dbPool); // Test aggregate result statistics - expect(result).toBeDefined(); - expect(result.numProposals).toEqual(2); - expect(result.sumNumOfHearts).toEqual(8); - expect(result.numOfParticipants).toEqual(2); - expect(result.numOfGroups).toEqual(1); + assert(result); + assert.equal(result.numProposals, 2, 'Number of proposals should be 2'); + assert.equal(result.sumNumOfHearts, 8); + assert.equal(result.numOfParticipants, 2, 'Number of participants should be 2'); + assert.equal(result.numOfGroups, 2, 'Number of groups should be 2'); // Test option stats - expect(result.optionStats).toBeDefined(); - expect(Object.keys(result.optionStats)).toHaveLength(2); + assert(result.optionStats, 'Option stats should not be empty'); + assert.equal(Object.keys(result.optionStats).length, 2, 'Number of options should be 2'); for (const optionId in result.optionStats) { const optionStat = result.optionStats[optionId]; - expect(optionStat).toBeDefined(); - expect(optionStat?.title).toBeDefined(); - expect(optionStat?.subTitle).toBeDefined(); - expect(optionStat?.pluralityScore).toBeDefined(); - expect(optionStat?.distinctUsers).toBeDefined(); - expect(optionStat?.allocatedHearts).toBeDefined(); - expect(optionStat?.quadraticScore).toBeDefined(); - expect(optionStat?.distinctGroups).toBeDefined(); - expect(optionStat?.listOfGroupNames).toBeDefined(); + assert(optionStat, 'Option stat should not be empty'); + assert(optionStat.title, 'Option title should not be empty'); // 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(8); - expect(optionStat?.quadraticScore).toEqual('4'); - expect(optionStat?.distinctGroups).toEqual(1); + assert.equal(optionStat?.distinctUsers, 2, 'Number of distinct users should be 2'); + assert.equal(optionStat?.allocatedHearts, 8, 'Number of allocated hearts should be 8'); + assert.equal(optionStat?.pluralityScore, '4', 'Plurality score should be 4'); + assert.equal(optionStat?.quadraticScore, 16), 'Quadratic score should be 16'; + assert.equal(optionStat?.distinctGroups, 1, 'Number of distinct groups should be 1'); const listOfGroupNames = optionStat?.listOfGroupNames; // Check if the array is not empty - expect(listOfGroupNames).toBeDefined(); - expect(listOfGroupNames?.length).toBeGreaterThan(0); + assert(listOfGroupNames, 'List of group names should not be empty'); } } }); - afterAll(async () => { - await cleanup(dbPool); - await dbConnection.end(); + after(async () => { + await deleteTestDatabase(); }); }); diff --git a/src/services/statistics.ts b/src/services/statistics.ts index 98c5c11c..f3854ee2 100644 --- a/src/services/statistics.ts +++ b/src/services/statistics.ts @@ -1,5 +1,5 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { sql } from 'drizzle-orm'; import { logger } from '../utils/logger'; @@ -25,14 +25,10 @@ type ResultData = { /** * 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 { NodePgDatabase} 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: NodePgDatabase, + dbPool: NodePgDatabase, ): Promise { try { // Execute all queries concurrently diff --git a/src/services/users-to-groups.spec.ts b/src/services/users-to-groups.spec.ts index 27017e43..4690ac87 100644 --- a/src/services/users-to-groups.spec.ts +++ b/src/services/users-to-groups.spec.ts @@ -1,68 +1,53 @@ -import * as db from '../db'; -import { createUsersToGroups, updateUsersToGroups } from './users-to-groups'; -import { eq, and } from 'drizzle-orm'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; -import { cleanup, seed } from '../utils/db/seed'; import { randUuid } from '@ngneat/falso'; +import { and, eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; +import assert from 'node:assert/strict'; +import { after, before, describe, test } from 'node:test'; +import { createTestDatabase, seed } from '../db'; +import * as schema from '../db/schema'; import { environmentVariables } from '../types'; +import { createUsersToGroups, updateUsersToGroups } from './users-to-groups'; describe('service: usersToGroups', function () { - let dbPool: NodePgDatabase; - let dbConnection: Client; - let user: db.User | undefined; - let defaultGroups: db.Group[]; - beforeAll(async function () { - const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); + let dbPool: NodePgDatabase; + let deleteTestDatabase: () => Promise; + let user: schema.User | undefined; + let defaultGroups: schema.Group[]; - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - dbPool = initDb.db; - dbConnection = initDb.client; + before(async function () { + const envVariables = environmentVariables.parse(process.env); + const { dbClient, teardown } = await createTestDatabase(envVariables); + dbPool = dbClient.db; + deleteTestDatabase = teardown; // seed const { users, groups } = await seed(dbPool); user = users[0]; - defaultGroups = groups.filter((group) => group !== undefined) as db.Group[]; + defaultGroups = groups.filter((group) => group !== undefined) as schema.Group[]; // insert users without group assignment - await dbPool.insert(db.users).values({ username: 'NewUser', email: 'SomeEmail' }); + await dbPool.insert(schema.users).values({ username: 'NewUser', email: 'SomeEmail' }); }); test('can save initial groups', async function () { // Get the newly inserted user const newUser = await dbPool.query.users.findFirst({ - where: eq(db.users.username, 'NewUser'), + where: eq(schema.users.username, 'NewUser'), }); await createUsersToGroups(dbPool, newUser?.id ?? '', defaultGroups[0]?.id ?? ''); // Find the userToGroup relationship for the newUser and the chosen group const newUserGroup = await dbPool.query.usersToGroups.findFirst({ - where: eq(db.usersToGroups.userId, newUser?.id ?? ''), + where: eq(schema.usersToGroups.userId, newUser?.id ?? ''), }); - expect(newUserGroup).toBeDefined(); - expect(newUserGroup?.userId).toBe(newUser?.id); + assert(newUserGroup); + assert.equal(newUserGroup.userId, 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'), + where: eq(schema.users.username, 'NewUser'), }); await createUsersToGroups(dbPool, newUser?.id ?? '', defaultGroups[2]?.id ?? ''); @@ -70,23 +55,23 @@ describe('service: usersToGroups', function () { // 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 ?? ''), + eq(schema.usersToGroups.userId, newUser?.id ?? ''), + eq(schema.usersToGroups.groupId, defaultGroups[2]?.id ?? ''), ), }); - expect(newUserGroup).toBeDefined(); - expect(newUserGroup?.userId).toBe(newUser?.id); - expect(newUserGroup?.groupId).toBe(defaultGroups[2]?.id); + assert(newUserGroup); + assert.equal(newUserGroup.userId, newUser?.id); + assert.equal(newUserGroup.groupId, defaultGroups[2]?.id); }); test('can update user groups', async function () { const newUser = await dbPool.query.users.findFirst({ - where: eq(db.users.username, 'NewUser'), + where: eq(schema.users.username, 'NewUser'), }); const userGroup = await dbPool.query.usersToGroups.findFirst({ - where: eq(db.usersToGroups.userId, newUser?.id ?? ''), + where: eq(schema.usersToGroups.userId, newUser?.id ?? ''), }); await updateUsersToGroups({ @@ -99,32 +84,32 @@ describe('service: usersToGroups', function () { // 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 ?? ''), + eq(schema.usersToGroups.userId, newUser?.id ?? ''), + eq(schema.usersToGroups.groupId, defaultGroups[1]?.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); + assert(newUserGroup); + assert(newUserGroup?.userId); + assert.equal(newUserGroup?.userId, newUser?.id); + assert.equal(newUserGroup?.groupId, defaultGroups[1]?.id); + assert.notEqual(newUserGroup?.groupId, defaultGroups[2]?.id); }); test('handles non-existent group IDs', async function () { const nonExistentGroupId = randUuid(); - await expect( + await assert.rejects( updateUsersToGroups({ dbPool, userId: user?.id ?? '', groupId: nonExistentGroupId, usersToGroupsId: '', }), - ).rejects.toThrow(); + ); }); - afterAll(async () => { - await cleanup(dbPool); - await dbConnection.end(); + after(async () => { + await deleteTestDatabase(); }); }); diff --git a/src/services/users-to-groups.ts b/src/services/users-to-groups.ts index 4a868e75..4a56bf23 100644 --- a/src/services/users-to-groups.ts +++ b/src/services/users-to-groups.ts @@ -1,24 +1,24 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { eq, and } from 'drizzle-orm'; import { logger } from '../utils/logger'; export async function createUsersToGroups( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, userId: string, groupId: string, ) { const group = await dbPool.query.groups.findFirst({ - where: eq(db.groups.id, groupId), + where: eq(schema.groups.id, groupId), }); if (!group) { - logger.error('Group not found with ID:', groupId); + logger.info('Group not found with ID:', groupId); throw new Error('Group not found'); } const existingUserToGroup = await dbPool.query.usersToGroups.findFirst({ - where: and(eq(db.usersToGroups.groupId, groupId), eq(db.usersToGroups.userId, userId)), + where: and(eq(schema.usersToGroups.groupId, groupId), eq(schema.usersToGroups.userId, userId)), }); if (existingUserToGroup) { @@ -27,7 +27,7 @@ export async function createUsersToGroups( } return await dbPool - .insert(db.usersToGroups) + .insert(schema.usersToGroups) .values({ userId, groupId, groupCategoryId: group.groupCategoryId }) .returning(); } @@ -38,22 +38,25 @@ export async function updateUsersToGroups({ userId, usersToGroupsId, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; usersToGroupsId: string; userId: string; groupId: string; }) { const group = await dbPool.query.groups.findFirst({ - where: eq(db.groups.id, groupId), + where: eq(schema.groups.id, groupId), }); if (!group) { - logger.error('Group not found with ID:', groupId); + logger.info('Group not found with ID:', groupId); throw new Error('Group not found'); } const existingAssociation = await dbPool.query.usersToGroups.findFirst({ - where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.id, usersToGroupsId)), + where: and( + eq(schema.usersToGroups.userId, userId), + eq(schema.usersToGroups.id, usersToGroupsId), + ), }); if (!existingAssociation) { @@ -61,14 +64,16 @@ export async function updateUsersToGroups({ } return await dbPool - .update(db.usersToGroups) + .update(schema.usersToGroups) .set({ userId, groupId, groupCategoryId: group.groupCategoryId, updatedAt: new Date() }) - .where(and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.id, usersToGroupsId))) + .where( + and(eq(schema.usersToGroups.userId, userId), eq(schema.usersToGroups.id, usersToGroupsId)), + ) .returning(); } export async function deleteUsersToGroups( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, userId: string, usersToGroupsId: string, ) { @@ -76,7 +81,10 @@ export async function deleteUsersToGroups( with: { groupCategory: true, }, - where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.id, usersToGroupsId)), + where: and( + eq(schema.usersToGroups.userId, userId), + eq(schema.usersToGroups.id, usersToGroupsId), + ), }); if (!groupToLeave) { @@ -84,7 +92,7 @@ export async function deleteUsersToGroups( } const userGroups = await dbPool.query.groups.findMany({ - where: eq(db.groups.groupCategoryId, groupToLeave.groupCategoryId!), + where: eq(schema.groups.groupCategoryId, groupToLeave.groupCategoryId!), }); // If the group is required and the user is only in one group, they cannot leave @@ -94,8 +102,8 @@ export async function deleteUsersToGroups( const isRegistrationAttached = await dbPool.query.registrations.findFirst({ where: and( - eq(db.registrations.userId, userId), - eq(db.registrations.groupId, groupToLeave.groupId), + eq(schema.registrations.userId, userId), + eq(schema.registrations.groupId, groupToLeave.groupId), ), }); @@ -104,7 +112,9 @@ export async function deleteUsersToGroups( } return await dbPool - .delete(db.usersToGroups) - .where(and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.id, usersToGroupsId))) + .delete(schema.usersToGroups) + .where( + and(eq(schema.usersToGroups.userId, userId), eq(schema.usersToGroups.id, usersToGroupsId)), + ) .returning(); } diff --git a/src/services/users.spec.ts b/src/services/users.spec.ts index 4873942e..ba64966e 100644 --- a/src/services/users.spec.ts +++ b/src/services/users.spec.ts @@ -1,16 +1,14 @@ -import * as db from '../db'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; -import { environmentVariables, insertUserSchema } from '../types'; -import { cleanup, seed } from '../utils/db/seed'; -import { updateUser, upsertUserData, validateUserData } from './users'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; +import assert from 'node:assert/strict'; +import { after, before, describe, test } from 'node:test'; import { z } from 'zod'; +import { createTestDatabase, seed } from '../db'; +import * as schema from '../db/schema'; +import { environmentVariables, insertUserSchema } from '../types'; +import { updateUser, upsertUserData, validateUserData } from './users'; describe('service: users', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; + let dbPool: NodePgDatabase; let userData: { email: string | null; username: string | null; @@ -18,28 +16,15 @@ describe('service: users', () => { lastName: string | null; telegram: string | null; }; - let user: db.User; - let secondUser: db.User; - beforeAll(async () => { + let user: schema.User; + let secondUser: schema.User; + let deleteTestDatabase: () => Promise; + + before(async () => { const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - dbPool = initDb.db; - dbConnection = initDb.client; + const { dbClient, teardown } = await createTestDatabase(envVariables); + dbPool = dbClient.db; + deleteTestDatabase = teardown; // seed const { users } = await seed(dbPool); user = users[0]!; @@ -60,7 +45,7 @@ describe('service: users', () => { // Loop through all keys and check if they are not empty strings for (const key of Object.keys(transformedUser)) { - expect(transformedUser[key]).not.toBe(''); + assert.notStrictEqual(transformedUser[key], ''); } }); @@ -74,8 +59,9 @@ describe('service: users', () => { }; const response = await validateUserData(dbPool, user?.id, userData); - expect(response).toBeDefined(); - expect(response).toEqual(expect.arrayContaining([expect.any(String)])); + assert(response !== null); + assert(response?.length > 0); + assert(response?.[0] !== null); }); test('validateUserData returns an error if username already exists', async () => { @@ -88,8 +74,9 @@ describe('service: users', () => { }; const response = await validateUserData(dbPool, user?.id, userData); - expect(response).toBeDefined(); - expect(response).toEqual(expect.arrayContaining([expect.any(String)])); + assert(response !== null); + assert(response?.length > 0); + assert(response?.[0] !== null); }); test('validateUserData returns null if validation is successful', async () => { @@ -102,7 +89,7 @@ describe('service: users', () => { }; const response = await validateUserData(dbPool, user?.id, userData); - expect(response).toBeNull(); + assert(response === null); }); test('upsertUserData returns updated user data if insertion is successful', async () => { @@ -115,11 +102,13 @@ describe('service: users', () => { }; const response = await upsertUserData(dbPool, user?.id, userData); - expect(response).toBeDefined(); - expect(Array.isArray(response)).toBe(true); - const updatedUser = response![0]; - expect(updatedUser!.firstName).toBe('Some Name'); - expect(updatedUser!.lastName).toBe('Some Other Name'); + assert(response); + assert(Array.isArray(response)); + assert(response.length > 0); + assert(response[0] !== null); + const updatedUser = response[0]; + assert.equal(updatedUser!.firstName, 'Some Name'); + assert.equal(updatedUser!.lastName, 'Some Other Name'); }); test('updateUser returns the respective error if validation fails', async () => { @@ -136,8 +125,9 @@ describe('service: users', () => { }; const response = await updateUser(dbPool, mockData); - expect(response.errors).toBeDefined(); - expect(response.errors![0]).toEqual(expect.any(String)); + assert(response.errors); + assert(response.errors?.length > 0); + assert(response.errors![0] !== null); }); test('updateUser returns user data if validation and insertion succeeds', async () => { @@ -154,13 +144,14 @@ describe('service: users', () => { }; const response = await updateUser(dbPool, mockData); - expect(response.data).toBeDefined(); - expect(response.data![0]!.firstName).toBe('Some Name'); - expect(response.data![0]!.lastName).toBe('Some Other Name'); + assert(response.data); + assert(response.data?.length > 0); + assert(response.data![0] !== null); + assert.equal(response.data![0]!.id, user?.id); + assert.equal(response.data![0]!.email, user?.email); }); - afterAll(async () => { - await cleanup(dbPool); - await dbConnection.end(); + after(async () => { + await deleteTestDatabase(); }); }); diff --git a/src/services/users.ts b/src/services/users.ts index f0034cc0..be1148f0 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -1,4 +1,4 @@ -import * as db from '../db'; +import * as schema from '../db/schema'; import { and, eq, ne, or } from 'drizzle-orm'; import { UserData, insertUserSchema } from '../types/users'; import { z } from 'zod'; @@ -7,24 +7,20 @@ import { logger } from '../utils/logger'; /** * Checks user data for existing entries in the database. - * @param { NodePgDatabase} dbPool - The database connection pool. - * @param {string} userId - The ID of the user to check. - * @param {UserData} userData - The user data to check. - * @returns {Promise | null>} - An array of errors if user data conflicts, otherwise null. */ export async function validateUserData( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, userId: string, userData: UserData, ) { if (userData.email || userData.username) { const existingUser = await dbPool .select() - .from(db.users) + .from(schema.users) .where( or( - and(eq(db.users.email, userData.email ?? ''), ne(db.users.id, userId)), - and(eq(db.users.username, userData.username ?? ''), ne(db.users.id, userId)), + and(eq(schema.users.email, userData.email ?? ''), ne(schema.users.id, userId)), + and(eq(schema.users.username, userData.username ?? ''), ne(schema.users.id, userId)), ), ); @@ -48,18 +44,15 @@ export async function validateUserData( /** * Upserts user data in the database. - * @param { NodePgDatabase} dbPool - The database connection pool. - * @param {string} userId - The ID of the user to update. - * @param {UserData} userData - The updated user data. */ export async function upsertUserData( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, userId: string, userData: UserData, ) { try { const user = await dbPool - .update(db.users) + .update(schema.users) .set({ email: userData.email, username: userData.username, @@ -68,7 +61,7 @@ export async function upsertUserData( telegram: userData.telegram, updatedAt: new Date(), }) - .where(eq(db.users.id, userId)) + .where(eq(schema.users.id, userId)) .returning(); return user; @@ -79,11 +72,9 @@ export async function upsertUserData( /** * Updates user data in the database. - * @param { NodePgDatabase} dbPool - The database connection pool. - * @returns {Function} - Express middleware function to handle the request. */ export async function updateUser( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: { userId: string; userData: z.infer; diff --git a/src/services/validation.spec.ts b/src/services/validation.spec.ts index 8c3be69f..347140d1 100644 --- a/src/services/validation.spec.ts +++ b/src/services/validation.spec.ts @@ -1,6 +1,8 @@ import { z } from 'zod'; import { dataSchema, fieldsSchema } from '../types'; import { enforceRules } from './validation'; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; describe('service: validation', function () { describe('rule: required', function () { @@ -20,8 +22,8 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(1); - expect(result).toEqual(['Name is required']); + assert.equal(result.length, 1); + assert.deepEqual(result, ['Name is required']); }); test('should not return an error if a required field is present', function () { @@ -46,7 +48,7 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(0); + assert.equal(result.length, 0); }); }); @@ -75,8 +77,8 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(1); - expect(result).toEqual(['Name must be at least 5 characters']); + assert.equal(result.length, 1); + assert.deepEqual(result, ['Name must be at least 5 characters']); }); test('should not return an error if the string is long enough', function () { @@ -102,7 +104,7 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(0); + assert.equal(result.length, 0); }); }); describe('rule: maxLength', function () { @@ -129,8 +131,8 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(1); - expect(result).toEqual(['Name must be at most 5 characters']); + assert.equal(result.length, 1); + assert.deepEqual(result, ['Name must be at most 5 characters']); }); test('should not return an error if the string is short enough', function () { @@ -156,7 +158,7 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(0); + assert.equal(result.length, 0); }); }); }); @@ -186,8 +188,8 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(1); - expect(result).toEqual(['Age must be at least 18']); + assert.equal(result.length, 1); + assert.deepEqual(result, ['Age must be at least 18']); }); test('should not return an error if the number is large enough', function () { @@ -213,7 +215,7 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(0); + assert.equal(result.length, 0); }); }); describe('rule: maxLength', function () { @@ -240,8 +242,8 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(1); - expect(result).toEqual(['Age must be at most 18']); + assert.equal(result.length, 1); + assert.deepEqual(result, ['Age must be at most 18']); }); test('should not return an error if the number is small enough', function () { const fields: z.infer = [ @@ -266,7 +268,7 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(0); + assert.equal(result.length, 0); }); }); }); @@ -296,8 +298,8 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(1); - expect(result).toEqual(['Colors must have at least 2 items']); + assert.equal(result.length, 1); + assert.deepEqual(result, ['Colors must have at least 2 items']); }); test('should not return an error if the array is large enough', function () { const fields: z.infer = [ @@ -322,7 +324,7 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(0); + assert.equal(result.length, 0); }); }); describe('rule: maxLength', function () { @@ -349,8 +351,8 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(1); - expect(result).toEqual(['Colors must have at most 2 items']); + assert.equal(result.length, 1); + assert.deepEqual(result, ['Colors must have at most 2 items']); }); test('should not return an error if the array is small enough', function () { const fields: z.infer = [ @@ -375,7 +377,7 @@ describe('service: validation', function () { const result = enforceRules({ data, fields }); - expect(result.length).toBe(0); + assert.equal(result.length, 0); }); }); }); diff --git a/src/services/votes.spec.ts b/src/services/votes.spec.ts index 4401e996..9ffc780c 100644 --- a/src/services/votes.spec.ts +++ b/src/services/votes.spec.ts @@ -1,62 +1,46 @@ -import * as db from '../db'; -import { createDbClient } from '../utils/db/create-db-connection'; -import { runMigrations } from '../utils/db/run-migrations'; +import { eq } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import assert from 'node:assert/strict'; +import { after, before, describe, test } from 'node:test'; +import { createTestDatabase, seed } from '../db'; +import * as schema from '../db/schema'; import { environmentVariables } from '../types'; -import { cleanup, seed } from '../utils/db/seed'; import { - saveVote, - validateVote, - queryVoteData, - queryGroupCategories, - numOfVotesDictionary, - groupsDictionary, calculatePluralScore, calculateQuadraticScore, + groupsDictionary, + numOfVotesDictionary, + queryGroupCategories, + queryVoteData, + saveVote, + updateOptionScore, updateVoteScoreInDatabase, updateVoteScorePlural, updateVoteScoreQuadratic, userCanVote, - updateOptionScore, + validateVote, } from './votes'; -import { eq } from 'drizzle-orm'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; describe('service: votes', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; + let dbPool: NodePgDatabase; + let deleteTestDatabase: () => Promise; let testData: { optionId: string; numOfVotes: number }; - let cycle: db.Cycle | undefined; - let questionOption: db.Option | undefined; - let otherQuestionOption: db.Option | undefined; - let forumQuestion: db.Question | undefined; - let otherForumQuestion: db.Question | 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; - beforeAll(async () => { + let cycle: schema.Cycle | undefined; + let questionOption: schema.Option | undefined; + let otherQuestionOption: schema.Option | undefined; + let forumQuestion: schema.Question | undefined; + let otherForumQuestion: schema.Question | undefined; + let groupCategory: schema.GroupCategory | undefined; + let otherGroupCategory: schema.GroupCategory | undefined; + let user: schema.User | undefined; + let secondUser: schema.User | undefined; + let thirdUser: schema.User | undefined; + + before(async () => { const envVariables = environmentVariables.parse(process.env); - const initDb = await createDbClient({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - await runMigrations({ - database: envVariables.DATABASE_NAME, - host: envVariables.DATABASE_HOST, - password: envVariables.DATABASE_PASSWORD, - user: envVariables.DATABASE_USER, - port: envVariables.DATABASE_PORT, - }); - - dbPool = initDb.db; - dbConnection = initDb.client; + const { dbClient, teardown } = await createTestDatabase(envVariables); + dbPool = dbClient.db; + deleteTestDatabase = teardown; // seed const { users, questionOptions, forumQuestions, cycles, groupCategories } = await seed(dbPool); // Insert registration fields for the user @@ -66,7 +50,6 @@ describe('service: votes', () => { otherForumQuestion = forumQuestions[1]; groupCategory = groupCategories[0]; otherGroupCategory = groupCategories[1]; - unrelatedGroupCategory = groupCategories[2]; user = users[0]; secondUser = users[1]; thirdUser = users[2]; @@ -79,8 +62,8 @@ describe('service: votes', () => { test('validation should return false if no option id is specified', async () => { const response = await validateVote(dbPool, { numOfVotes: 1, optionId: '' }, user!.id ?? ''); - expect(response.isValid).toEqual(false); - expect(response.error).toEqual(expect.any(String)); + assert.equal(response.isValid, false); + assert(response.error); }); test('validation should return false if a non-existing optionid is specified', async () => { @@ -89,34 +72,41 @@ describe('service: votes', () => { { numOfVotes: 1, optionId: '00000000-0000-0000-0000-000000000000' }, user!.id ?? '', ); - expect(response.isValid).toEqual(false); - expect(response.error).toEqual(expect.any(String)); + assert.equal(response.isValid, false); + assert(response.error); }); test('validation should return false if the cycle is not open', async () => { - await dbPool.update(db.cycles).set({ status: 'CLOSED' }).where(eq(db.cycles.id, cycle!.id)); + await dbPool + .update(schema.cycles) + .set({ status: 'CLOSED' }) + .where(eq(schema.cycles.id, cycle!.id)); const response = await validateVote( dbPool, { numOfVotes: 1, optionId: questionOption?.id ?? '' }, user!.id ?? '', ); - expect(response.isValid).toEqual(false); - expect(response.error).toEqual(expect.any(String)); + assert.equal(response.isValid, false); + assert(response.error); }); test('validation should return false if a user is not approved', async () => { - await dbPool.update(db.cycles).set({ status: 'OPEN' }).where(eq(db.cycles.id, cycle!.id)); + await dbPool + .update(schema.cycles) + .set({ status: 'OPEN' }) + .where(eq(schema.cycles.id, cycle!.id)); const response = await validateVote( dbPool, { numOfVotes: 1, optionId: questionOption?.id ?? '' }, user!.id ?? '', ); - expect(response.isValid).toEqual(false); - expect(response.error).toEqual(expect.any(String)); + + assert.equal(response.isValid, false); + assert(response.error); }); test('validation should return true all validation checks pass', async () => { - await dbPool.insert(db.registrations).values({ + await dbPool.insert(schema.registrations).values({ status: 'APPROVED', userId: user!.id ?? '', eventId: cycle!.eventId ?? '', @@ -126,28 +116,29 @@ describe('service: votes', () => { { numOfVotes: 1, optionId: questionOption?.id ?? '' }, user!.id ?? '', ); - expect(response.isValid).toEqual(true); - expect(response.error).toEqual(null); + + assert.equal(response.isValid, true); + assert.equal(response.error, null); }); test('userCanVote returns false if user does not have an approved registration', async () => { const response = await userCanVote(dbPool, secondUser!.id ?? '', questionOption?.id ?? ''); - expect(response).toEqual(false); + assert.equal(response, false); }); test('userCanVote returns true if user has an approved registration', async () => { - await dbPool.insert(db.registrations).values({ + await dbPool.insert(schema.registrations).values({ status: 'APPROVED', userId: secondUser!.id ?? '', eventId: cycle!.eventId ?? '', }); const response = await userCanVote(dbPool, secondUser!.id ?? '', questionOption?.id ?? ''); - expect(response).toEqual(true); + assert.equal(response, true); }); test('userCanVote returns false if no option id gets provided', async () => { const response = await userCanVote(dbPool, secondUser!.id ?? '', ''); - expect(response).toEqual(false); + assert.equal(response, false); }); test('should save vote', async () => { @@ -157,13 +148,14 @@ describe('service: votes', () => { user?.id ?? '', forumQuestion?.id ?? '', ); - expect(response).toBeDefined(); - expect(response).toHaveProperty('id'); - expect(response?.id).toEqual(expect.any(String)); - expect(response).toHaveProperty('userId'); - expect(response?.userId).toEqual(expect.any(String)); - expect(response?.createdAt).toEqual(expect.any(Date)); - expect(response?.updatedAt).toEqual(expect.any(Date)); + assert(response); + assert(response?.id); + assert(response?.userId); + assert(response?.optionId); + assert(response?.numOfVotes); + assert(response?.questionId); + assert(response?.createdAt); + assert(response?.updatedAt); }); test('should not save vote with invalid test data', async () => { @@ -177,9 +169,8 @@ describe('service: votes', () => { user?.id ?? '', forumQuestion?.id ?? '', ); - expect(response.data).toBeNull(); - expect(response.error).toBeDefined(); - expect(response.error).toEqual(expect.any(String)); + assert.equal(response.data, null); + assert(response.error); }); test('UpdateOptionScore returns an error if not all question ids are the same', async () => { @@ -191,9 +182,9 @@ describe('service: votes', () => { const questionIds = [forumQuestion?.id ?? '', otherForumQuestion?.id ?? '']; const response = await updateOptionScore(dbPool, mockData, questionIds); - expect(response.data).toBeNull(); - expect(response.errors).toBeDefined(); - expect(response.errors[0]).toEqual(expect.any(String)); + assert.equal(response.data, null); + assert(response.errors); + assert(response.errors[0]); }); test('UpdateOptionScore returns an error if no question id is found', async () => { @@ -208,9 +199,9 @@ describe('service: votes', () => { ]; const response = await updateOptionScore(dbPool, mockData, questionIds); - expect(response.data).toBeNull(); - expect(response.errors).toBeDefined(); - expect(response.errors[0]).toEqual(expect.any(String)); + assert.equal(response.data, null); + assert(response.errors); + assert(response.errors[0]); }); test('UpdateOptionScore returns an error if a valid but non existing uuid gets provided', async () => { @@ -222,14 +213,14 @@ describe('service: votes', () => { const questionIds = ['', '']; const response = await updateOptionScore(dbPool, mockData, questionIds); - expect(response.data).toBeNull(); - expect(response.errors).toBeDefined(); - expect(response.errors[0]).toEqual(expect.any(String)); + assert.equal(response.data, null); + assert(response.errors); + assert(response.errors[0]); }); test('should fetch vote data correctly', async () => { // register second user - await dbPool.insert(db.registrations).values({ + await dbPool.insert(schema.registrations).values({ status: 'APPROVED', userId: secondUser!.id ?? '', eventId: cycle!.eventId ?? '', @@ -237,14 +228,17 @@ describe('service: votes', () => { await saveVote(dbPool, testData, secondUser!.id, forumQuestion?.id ?? ''); const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); - expect(voteArray).toBeDefined(); - expect(voteArray).toHaveLength(2); + assert(voteArray); + assert.equal(voteArray.length, 2); + voteArray?.forEach((vote) => { - expect(vote).toHaveProperty('userId'); - expect(vote).toHaveProperty('numOfVotes'); - expect(typeof vote.numOfVotes).toBe('number'); + assert(vote); + assert(vote.userId); + assert(vote.numOfVotes); + assert(Number.isInteger(vote.numOfVotes)); }); - expect(voteArray[0]?.numOfVotes).toBe(1); + + assert.equal(voteArray[0]?.numOfVotes, 1); }); test('should transform voteArray correctly', () => { @@ -257,10 +251,10 @@ describe('service: votes', () => { ]; const result = numOfVotesDictionary(voteArray); - expect(result).toEqual({ - user1: 10, - user3: 5, - }); + assert(result); + assert.equal(Object.keys(result).length, 2); + assert.equal(result.user1, 10); + assert.equal(result.user3, 5); }); test('should include users with zero votes if there are no non-zero votes', () => { @@ -271,15 +265,15 @@ describe('service: votes', () => { ]; const result = numOfVotesDictionary(voteArray); - expect(result).toEqual({ - user1: 0, - user2: 0, - }); + assert(result); + assert.equal(Object.keys(result).length, 2); + assert.equal(result.user1, 0); + assert.equal(result.user2, 0); }); test('vote dictionary should not contain users voting for another option', async () => { // create vote for another question option - await dbPool.insert(db.votes).values({ + await dbPool.insert(schema.votes).values({ numOfVotes: 5, optionId: otherQuestionOption!.id, questionId: forumQuestion!.id, @@ -289,26 +283,27 @@ describe('service: votes', () => { const voteArray = await queryVoteData(dbPool, questionOption?.id ?? ''); const result = await numOfVotesDictionary(voteArray); - expect(user!.id in result).toBe(true); - expect(secondUser!.id in result).toBe(true); - expect(thirdUser!.id in result).toBe(false); + assert(user); + assert(secondUser); + assert(thirdUser); + assert.equal(user.id in result, true); + assert.equal(secondUser.id in result, true); + assert.equal(thirdUser.id in result, 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.data!.length).toBe(1); - expect(Array.isArray(groupCategoriesIdArray.data)).toBe(true); - groupCategoriesIdArray.data!.forEach((categoryId) => { - expect(typeof categoryId).toBe('string'); - }); + assert(groupCategoriesIdArray); + assert(groupCategoriesIdArray.data); + assert.equal(groupCategoriesIdArray.data.length, 1); + assert.equal(typeof groupCategoriesIdArray.data[0], 'string'); }); 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.data!).toBe(null); + assert(groupCategoriesIdArray); + assert.equal(groupCategoriesIdArray.data, null); }); test('only return groups for users who voted for the option', async () => { @@ -316,11 +311,15 @@ 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(1); - expect(groups[Object.keys(groups)[0]!]!.length).toEqual(2); + assert(groups); + assert(groups['unexpectedKey'] === undefined); + assert(typeof groups === 'object'); + // check that the groups dictionary only has user ids from the votes dictionary + for (const key in groups) { + for (const userId of groups[key]!) { + assert(userId in votesDictionary, `User ${userId} not in votes dictionary`); + } + } }); test('only return groups for users who voted for the option with two elidgible group categories', async () => { @@ -331,27 +330,11 @@ describe('service: votes', () => { 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); + assert(groups); + assert(groups['unexpectedKey'] === undefined); + assert(typeof groups === 'object'); + assert.equal(Object.keys(groups).length, 2); + assert.equal(groups[Object.keys(groups)[0]!]!.length, 2); }); test('should calculate the plural score correctly', () => { @@ -371,7 +354,9 @@ describe('service: votes', () => { }; const result = calculatePluralScore(groupsDictionary, numOfVotesDictionary); - expect(result).toBe(4.597873224984399); + assert(result); + assert.equal(typeof result, 'number'); + assert.equal(result, 4.597873224984399); }); test('plural score should be 0 when every user vote is zero', () => { @@ -391,7 +376,8 @@ describe('service: votes', () => { }; const result = calculatePluralScore(groupsDictionary, numOfVotesDictionary); - expect(result).toBe(0); + assert.equal(typeof result, 'number'); + assert.equal(result, 0); }); test('test quadratic score calculation', () => { @@ -404,7 +390,9 @@ describe('service: votes', () => { }; const result = calculateQuadraticScore(numOfVotesDictionary); - expect(result).toBe(10); + assert(result); + assert.equal(typeof result, 'number'); + assert.equal(result, 10); }); test('update vote score in database', async () => { @@ -412,12 +400,14 @@ describe('service: votes', () => { const score = 100; await updateVoteScoreInDatabase(dbPool, questionOption?.id ?? '', score); - // query updated score in db + // query updated db in schema const updatedDbScore = await dbPool.query.options.findFirst({ - where: eq(db.options.id, questionOption?.id ?? ''), + where: eq(schema.options.id, questionOption?.id ?? ''), }); - expect(updatedDbScore?.voteScore).toBe('100'); + assert(updatedDbScore); + assert(updatedDbScore.voteScore); + assert.equal(updatedDbScore.voteScore, '100'); }); test('that the plurality score is correct if both users are in the same group', async () => { @@ -428,18 +418,17 @@ describe('service: votes', () => { ); // sqrt of 2 because the two users are in the same group // voting for the same option with 1 vote each - expect(score).toBe(Math.sqrt(2)); + assert.equal(score, Math.sqrt(2)); }); test('that the quadratic score is correctly calculated as the sum of square roots', async () => { const score = await updateVoteScoreQuadratic(dbPool, questionOption?.id ?? ''); // two users voting for the same option with 1 vote each // sqrt of 1 + sqrt of 1 = 2 - expect(score).toBe(2); + assert.equal(score, 2); }); - afterAll(async () => { - await cleanup(dbPool); - await dbConnection.end(); + after(async () => { + await deleteTestDatabase(); }); }); diff --git a/src/services/votes.ts b/src/services/votes.ts index 08f3c797..9f062f22 100644 --- a/src/services/votes.ts +++ b/src/services/votes.ts @@ -1,6 +1,5 @@ import { and, eq, sql } from 'drizzle-orm'; -import * as db from '../db'; -import { votes } from '../db/votes'; +import * as schema from '../db/schema'; import { PluralVoting } from '../modules/plural-voting'; import { insertVotesSchema } from '../types'; import { CycleStatusType } from '../types/cycles'; @@ -13,18 +12,13 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; * * This function validates each vote provided in the `data` array for the specified `userId`. * If all votes are valid, it saves each vote to the database. - * - * @param {NodePgDatabase} dbPool - * @param {{ optionId: string; numOfVotes: number }[]} data - * @param {string} userId - * @returns {Promise<{ data: db.Vote[] | null; errors: string[] }>} */ export async function validateAndSaveVotes( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: { optionId: string; numOfVotes: number }[], userId: string, -): Promise<{ data: db.Vote[] | null; questionIds: string[]; errors: string[] }> { - const voteData: db.Vote[] = []; +): Promise<{ data: schema.Vote[] | null; questionIds: string[]; errors: string[] }> { + const voteData: schema.Vote[] = []; const questionIds: string[] = []; const errors: string[] = []; @@ -38,7 +32,7 @@ export async function validateAndSaveVotes( // Find the question option if the vote is valid const queryQuestionOption = await dbPool.query.options.findFirst({ - where: eq(db.options.id, vote.optionId), + where: eq(schema.options.id, vote.optionId), }); if (!queryQuestionOption) { @@ -66,14 +60,9 @@ export async function validateAndSaveVotes( /** * Updates option scores based on the vote model. - * - * @param {NodePgDatabase} dbPool - * @param {{ optionId: string; numOfVotes: number }[]} data - * @param {string[]} questionIds - * @returns {Promise<{ data: { optionId: string; score: number }[] | null; errors: string[] }>} */ export async function updateOptionScore( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: { optionId: string; numOfVotes: number }[], questionIds: string[], ): Promise<{ data: { optionId: string; score: number }[] | null; errors: string[] }> { @@ -95,11 +84,11 @@ export async function updateOptionScore( // Query group data, grouping dimensions, and calculate the score const queryQuestion = await dbPool .select({ - questionId: db.questions.id, - voteModel: db.questions.voteModel, + questionId: schema.questions.id, + voteModel: schema.questions.voteModel, }) - .from(db.questions) - .where(eq(db.questions.id, firstQuestionId)); + .from(schema.questions) + .where(eq(schema.questions.id, firstQuestionId)); if (queryQuestion.length === 0) { errors.push('No question found for the provided questionId'); @@ -114,7 +103,7 @@ export async function updateOptionScore( optionId, questionId, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; optionId: string; questionId: string; }): Promise; @@ -149,10 +138,10 @@ export async function updateOptionScore( /** Queries latest vote data by users for a specified option ID. -@param { NodePgDatabase} dbPool - The database connection pool. +@param { NodePgDatabase} dbPool - The database connection pool. @param {string} optionId - The ID of the option for which to query vote data. */ -export async function queryVoteData(dbPool: NodePgDatabase, optionId: string) { +export async function queryVoteData(dbPool: NodePgDatabase, optionId: string) { const voteArray = await dbPool.execute<{ userId: string; numOfVotes: number }>( sql.raw(` SELECT user_id AS "userId", num_of_votes AS "numOfVotes" @@ -196,15 +185,15 @@ export function numOfVotesDictionary(voteArray: Array<{ userId: string; numOfVot * - `error`: A string describing the error if no group categories are found, otherwise `null`. */ export async function queryGroupCategories( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, questionId: string, ): Promise<{ data: string[] | null; error: string | null }> { const groupCategories = await dbPool .select({ - groupCategoryId: db.questionsToGroupCategories.groupCategoryId, + groupCategoryId: schema.questionsToGroupCategories.groupCategoryId, }) - .from(db.questionsToGroupCategories) - .where(eq(db.questionsToGroupCategories.questionId, questionId)); + .from(schema.questionsToGroupCategories) + .where(eq(schema.questionsToGroupCategories.questionId, questionId)); if (groupCategories.length === 0) { return { data: null, error: 'No group categories found for the given question Id' }; @@ -217,12 +206,9 @@ export async function queryGroupCategories( /** * Queries group data and creates group dictionary based on user IDs and option ID. - * @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: NodePgDatabase, + dbPool: NodePgDatabase, numOfVotesDictionary: Record, groupCategories: Array, ) { @@ -273,22 +259,22 @@ export function calculateQuadraticScore(numOfVotesDictionary: Record} dbPool - The database connection pool. +@param { NodePgDatabase} dbPool - The database connection pool. @param {string} optionId - The ID of the option for which to update the vote score. @param {number} score - The new vote score to be set. */ export async function updateVoteScoreInDatabase( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, optionId: string, score: number, ) { await dbPool - .update(db.options) + .update(schema.options) .set({ voteScore: score.toString(), updatedAt: new Date(), }) - .where(eq(db.options.id, optionId)); + .where(eq(schema.options.id, optionId)); } /** @@ -298,11 +284,11 @@ export async function updateVoteScoreInDatabase( * combines them, calculates the score using plural voting, updates * the vote score in the database, and returns the calculated score. * - * @param { NodePgDatabase} dbPool - The database connection pool. + * @param { NodePgDatabase} dbPool - The database connection pool. * @param {string} optionId - The ID of the option for which to update the vote score. */ export async function updateVoteScorePlural( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, optionId: string, questionId: string, ): Promise { @@ -325,11 +311,11 @@ export async function updateVoteScorePlural( * combines them, calculates the score using quadratic voting, updates * the vote score in the database, and returns the calculated score. * - * @param { NodePgDatabase} dbPool - The database connection pool. + * @param { NodePgDatabase} dbPool - The database connection pool. * @param {string} optionId - The ID of the option for which to update the vote score. */ export async function updateVoteScoreQuadratic( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, optionId: string, ): Promise { const voteArray = await queryVoteData(dbPool, optionId); @@ -349,14 +335,9 @@ export async function updateVoteScoreQuadratic( * 4. Checks if the user is eligible to vote. * * If all validations pass, the vote is considered valid. Otherwise, an appropriate error message is returned. - * - * @param {NodePgDatabase} dbPool - * @param {{ optionId: string; numOfVotes: number }} vote - * @param {string} userId - * @returns {Promise<{ isValid: boolean; error: string | null }>} */ export async function validateVote( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, vote: { optionId: string; numOfVotes: number }, userId: string, ): Promise<{ isValid: boolean; error: string | null }> { @@ -366,7 +347,7 @@ export async function validateVote( // check if the option exists const queryQuestionOption = await dbPool.query.options.findFirst({ - where: eq(db.options.id, vote.optionId), + where: eq(schema.options.id, vote.optionId), }); if (!queryQuestionOption) { @@ -375,7 +356,7 @@ export async function validateVote( // check cycle status const queryQuestion = await dbPool.query.questions.findFirst({ - where: eq(db.questions.id, queryQuestionOption.questionId), + where: eq(schema.questions.id, queryQuestionOption.questionId), with: { cycle: true, }, @@ -397,17 +378,13 @@ export async function validateVote( /** * Saves a vote in the database. * - * @param { NodePgDatabase} dbPool - * @param {z.infer} vote - * @param {string} userId - * @param {string} questionId */ export async function saveVote( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, vote: { optionId: string; numOfVotes: number }, userId: string, questionId: string, -): Promise<{ data: db.Vote | null; error: string | null | undefined }> { +): Promise<{ data: schema.Vote | null; error: string | null | undefined }> { const insertVoteBody: z.infer = { optionId: vote.optionId, numOfVotes: vote.numOfVotes, @@ -423,7 +400,7 @@ export async function saveVote( // save the votes const newVote = await dbPool - .insert(votes) + .insert(schema.votes) .values({ userId: insertVoteBody.userId, numOfVotes: insertVoteBody.numOfVotes, @@ -441,13 +418,13 @@ export async function saveVote( /** * Checks whether a user can vote on an option based on their registration status. - * @param { NodePgDatabase} dbPool - The PostgreSQL database pool. + * @param { NodePgDatabase} dbPool - The PostgreSQL database pool. * @param {string} userId - The ID of the user attempting to vote. * @param {string} optionId - The ID of the option to be voted on. * @returns {Promise} A promise that resolves to true if the user can vote on the option, false otherwise. */ export async function userCanVote( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, userId: string, optionId: string, ) { @@ -457,10 +434,12 @@ export async function userCanVote( // check if user has an approved registration const res = await dbPool .selectDistinct({ - user: db.registrations.userId, + user: schema.registrations.userId, }) - .from(db.registrations) - .where(and(eq(db.registrations.userId, userId), eq(db.registrations.status, 'APPROVED'))); + .from(schema.registrations) + .where( + and(eq(schema.registrations.userId, userId), eq(schema.registrations.status, 'APPROVED')), + ); if (!res.length) { return false; diff --git a/src/types/comments.ts b/src/types/comments.ts index 594f6da9..1a6e4a50 100644 --- a/src/types/comments.ts +++ b/src/types/comments.ts @@ -1,4 +1,4 @@ import { createInsertSchema } from 'drizzle-zod'; -import { comments } from '../db/comments'; +import { comments } from '../db/schema/comments'; export const insertCommentSchema = createInsertSchema(comments); diff --git a/src/types/groups.ts b/src/types/groups.ts index 4e897fa0..3479da37 100644 --- a/src/types/groups.ts +++ b/src/types/groups.ts @@ -1,5 +1,5 @@ import { createInsertSchema } from 'drizzle-zod'; -import { groups } from '../db'; +import { groups } from '../db/schema'; import { z } from 'zod'; export const insertGroupsSchema = createInsertSchema(groups, { diff --git a/src/types/options.ts b/src/types/options.ts index b9c7ccc7..b2c1eb9e 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -1,6 +1,6 @@ import { createInsertSchema } from 'drizzle-zod'; import { dataSchema } from './validation'; -import { options } from '../db'; +import { options } from '../db/schema'; export const insertOptionsSchema = createInsertSchema(options, { data: dataSchema, diff --git a/src/types/registrations.ts b/src/types/registrations.ts index e0e0d6cf..8e29d865 100644 --- a/src/types/registrations.ts +++ b/src/types/registrations.ts @@ -1,5 +1,5 @@ import { createInsertSchema } from 'drizzle-zod'; -import { registrations } from '../db/registrations'; +import { registrations } from '../db/schema/registrations'; import { dataSchema } from './validation'; export const insertRegistrationSchema = createInsertSchema(registrations, { diff --git a/src/types/users.ts b/src/types/users.ts index d134e246..93b31e62 100644 --- a/src/types/users.ts +++ b/src/types/users.ts @@ -1,6 +1,6 @@ import { createInsertSchema } from 'drizzle-zod'; import { z } from 'zod'; -import { users } from '../db'; +import { users } from '../db/schema'; export const insertUserSchema = createInsertSchema(users).transform((data) => { // make empty strings null diff --git a/src/types/votes.ts b/src/types/votes.ts index b9c9bab9..7627dce2 100644 --- a/src/types/votes.ts +++ b/src/types/votes.ts @@ -1,4 +1,4 @@ import { createInsertSchema } from 'drizzle-zod'; -import { votes } from '../db/votes'; +import { votes } from '../db/schema/votes'; export const insertVotesSchema = createInsertSchema(votes); diff --git a/src/utils/db/seed-data-generators.ts b/src/utils/db/seed-data-generators.ts deleted file mode 100644 index 6f2b0c25..00000000 --- a/src/utils/db/seed-data-generators.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { randCompanyName, randCountry, randUser, randUuid } from '@ngneat/falso'; -import { - Cycle, - Event, - Question, - RegistrationField, - RegistrationFieldOption, - Option, - GroupCategory, - Group, - User, - UsersToGroups, - QuestionsToGroupCategories, -} from '../../db'; -import { z } from 'zod'; -import { fieldsSchema } from '../../types'; - -// Define types -export type CycleData = Pick; -export type EventData = Pick; -export type RegistrationFieldData = Pick< - RegistrationField, - 'name' | 'eventId' | 'type' | 'required' | 'forUser' | 'forGroup' ->; -export type RegistrationFieldOptionData = Pick< - RegistrationFieldOption, - 'registrationFieldId' | 'value' ->; -export type ForumQuestionData = Pick; -export type QuestionOptionData = Pick; -export type GroupCategoryData = Pick< - GroupCategory, - 'name' | 'eventId' | 'userCanCreate' | 'userCanView' | 'required' ->; -export type GroupData = Pick; -export type UserData = Pick; -export type UsersToGroupsData = Pick; -export type QuestionsToGroupCategoriesData = Pick< - QuestionsToGroupCategories, - 'questionId' | 'groupCategoryId' ->; - -export function generateEventData(numEvents: number): EventData[] { - const events: EventData[] = []; - const fields: z.infer = [ - { - id: randUuid(), - name: 'What do you think about this event?', - position: 0, - type: 'TEXT', - validation: { - required: true, - }, - }, - { - id: randUuid(), - name: 'Opinion', - description: 'Optional opinion', - position: 1, - type: 'TEXT', - validation: { - required: false, - }, - }, - ]; - - for (let i = 0; i < numEvents; i++) { - events.push({ name: randCountry(), fields }); - } - return events; -} - -export function generateCycleData(numCycles: number, eventId: string): CycleData[] { - const cycles: CycleData[] = []; - const today = new Date(); - - for (let i = 0; i < numCycles; i++) { - const startAt = new Date(today); - const endAt = new Date(startAt); - endAt.setDate(startAt.getDate() + 1); - - cycles.push({ - startAt, - endAt, - status: 'OPEN', - eventId, - }); - } - - return cycles; -} - -export function generateRegistrationFieldData( - eventId: string, - fields: Partial[] = [], -): RegistrationFieldData[] { - return fields.map((field) => ({ - name: field.name || 'Untitled Field', - type: field.type || 'TEXT', - required: field.required !== undefined ? field.required : false, - eventId, - forUser: field.forUser !== undefined ? field.forUser : false, - forGroup: field.forGroup !== undefined ? field.forGroup : false, - })); -} - -export function generateRegistrationFieldOptionsData( - registrationFieldId: string, - options: string[], -): RegistrationFieldOptionData[] { - return options.map((option) => ({ - registrationFieldId, - value: option, - })); -} - -export function generateForumQuestionData( - cycleId: string, - questionTitles: string[], - voteModels: string[], -): ForumQuestionData[] { - return questionTitles.map((title, index) => ({ - cycleId, - title, - voteModel: voteModels[index] ?? 'COCM', - })); -} - -export function generateQuestionOptionsData( - questionId: string, - optionTitles: string[], - status: boolean[], -): QuestionOptionData[] { - const questionOptionsData: QuestionOptionData[] = []; - - for (let i = 0; i < optionTitles.length; i++) { - const optionData: QuestionOptionData = { - questionId, - title: optionTitles[i]!, - show: status[i]!, - }; - questionOptionsData.push(optionData); - } - - return questionOptionsData; -} - -export function generateGroupCategoryData( - eventId: string, - categories: Partial[] = [], -): GroupCategoryData[] { - return categories.map((category) => ({ - name: category.name || 'Untitled Category', - eventId, - userCanView: category.userCanView !== undefined ? category.userCanView : true, - userCanCreate: category.userCanCreate !== undefined ? category.userCanCreate : false, - required: category.required !== undefined ? category.required : false, - })); -} - -export function generateGroupData( - categoryIds: string[], - numGroupsPerCategory: number[], -): GroupData[] { - const groupData: GroupData[] = []; - - categoryIds.forEach((categoryId, index) => { - const numGroups = numGroupsPerCategory[index]!; - for (let i = 0; i < numGroups; i++) { - const data: GroupData = { - name: randCompanyName(), - groupCategoryId: categoryId, - }; - groupData.push(data); - } - }); - - return groupData; -} - -export function generateUserData(numUsers: number): UserData[] { - const users: UserData[] = []; - for (let i = 0; i < numUsers; i++) { - users.push({ - username: randUser().username, - email: randUser().email, - firstName: randUser().firstName, - lastName: randUser().lastName, - }); - } - return users; -} - -export function generateUsersToGroupsData( - userIds: string[], - groupIds: string[], - categoryIds: string[], -): UsersToGroupsData[] { - const usersToGroupsData: UsersToGroupsData[] = []; - - for (let i = 0; i < userIds.length; i++) { - const Data: UsersToGroupsData = { - userId: userIds[i]!, - groupId: groupIds[i]!, - groupCategoryId: categoryIds[i]!, - }; - usersToGroupsData.push(Data); - } - - return usersToGroupsData; -} - -export function generateQuestionsToGroupCategoriesData( - questionIds: string[], - categoryIds: string[], -): QuestionsToGroupCategoriesData[] { - const questionsToCategoriesData: QuestionsToGroupCategoriesData[] = []; - - for (let i = 0; i < categoryIds.length; i++) { - const Data: QuestionsToGroupCategoriesData = { - questionId: questionIds[i]!, - groupCategoryId: categoryIds[i]!, - }; - questionsToCategoriesData.push(Data); - } - - return questionsToCategoriesData; -} diff --git a/src/utils/db/seed.ts b/src/utils/db/seed.ts deleted file mode 100644 index 4b75b041..00000000 --- a/src/utils/db/seed.ts +++ /dev/null @@ -1,440 +0,0 @@ -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import * as db from '../../db'; -import { - EventData, - CycleData, - RegistrationFieldData, - RegistrationFieldOptionData, - ForumQuestionData, - QuestionOptionData, - GroupCategoryData, - GroupData, - UserData, - UsersToGroupsData, - QuestionsToGroupCategoriesData, - generateEventData, - generateCycleData, - generateRegistrationFieldData, - generateRegistrationFieldOptionsData, - generateForumQuestionData, - generateQuestionOptionsData, - generateGroupCategoryData, - generateGroupData, - generateUserData, - generateUsersToGroupsData, - generateQuestionsToGroupCategoriesData, -} from './seed-data-generators'; - -async function seed(dbPool: NodePgDatabase) { - const events = await createEvent(dbPool, generateEventData(1)); - const cycles = await createCycle(dbPool, generateCycleData(1, events[0]!.id)); - const registrationFieldsData = [ - { name: 'proposal title', type: 'TEXT', required: true, forGroup: true }, - { name: 'proposal description', type: 'TEXT', required: true, forUser: true }, - { name: 'other field', type: 'TEXT', required: false }, - { name: 'select field', type: 'SELECT', required: false, forUser: true }, - ]; - const registrationFields = await createRegistrationFields( - dbPool, - generateRegistrationFieldData(events[0]!.id, registrationFieldsData), - ); - const registrationFieldOptions = await createRegistrationFieldOptions( - dbPool, - generateRegistrationFieldOptionsData(registrationFields[3]!.id, ['Option A', 'Option B']), - ); - const forumQuestions = await createForumQuestions( - dbPool, - generateForumQuestionData(cycles[0]!.id, ['Question One', 'Question Two'], ['COCM', 'QV']), - ); - const questionOptions = await createQuestionOptions( - dbPool, - generateQuestionOptionsData(forumQuestions[0]!.id, ['Option A', 'Option B'], [true, true]), - ); - - const groupCategoriesData = [ - { name: 'affiliation', userCanView: true, required: true }, - { name: 'public', userCanView: true, userCanCreate: false }, - { name: 'secrets', userCanCreate: true, userCanView: false }, - { name: 'tension', userCanCreate: true, userCanView: true, require: false }, - ]; - - const groupCategories = await createGroupCategories( - dbPool, - generateGroupCategoryData(events[0]!.id, groupCategoriesData), - ); - - const categoryIdsData = [ - groupCategories[0]!.id, - groupCategories[1]!.id, - groupCategories[2]!.id, - groupCategories[3]!.id, - ]; - const numOfGroupsData = [5, 4, 3, 5]; - - const groups = await createGroups(dbPool, generateGroupData(categoryIdsData, numOfGroupsData)); - - const users = await createUsers(dbPool, generateUserData(3)); - - // Specify users to groups relationships - const userData = [ - users[0]!.id, - users[1]!.id, - users[2]!.id, - users[0]!.id, - users[1]!.id, - users[2]!.id, - ]; - const groupData = [ - groups[0]!.id, - groups[0]!.id, - groups[0]!.id, - groups[1]!.id, - groups[1]!.id, - groups[2]!.id, - ]; - const categoryData = [ - groupCategories[0]!.id, - groupCategories[0]!.id, - groupCategories[0]!.id, - groupCategories[1]!.id, - groupCategories[1]!.id, - groupCategories[1]!.id, - ]; - - const usersToGroups = await createUsersToGroups( - dbPool, - generateUsersToGroupsData(userData, groupData, categoryData), - ); - - const questionsToGroupCategories = await createQuestionsToGroupCategories( - dbPool, - generateQuestionsToGroupCategoriesData([forumQuestions[0]!.id], [groupCategories[0]!.id]), - ); - - return { - events, - cycles, - registrationFields, - registrationFieldOptions, - forumQuestions, - questionOptions, - groupCategories, - groups, - users, - usersToGroups, - questionsToGroupCategories, - }; -} - -async function cleanup(dbPool: NodePgDatabase) { - await dbPool.delete(db.userAttributes); - await dbPool.delete(db.votes); - await dbPool.delete(db.federatedCredentials); - await dbPool.delete(db.options); - 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); - await dbPool.delete(db.users); - await dbPool.delete(db.groups); - await dbPool.delete(db.questionsToGroupCategories); - await dbPool.delete(db.groupCategories); - await dbPool.delete(db.questions); - await dbPool.delete(db.cycles); - await dbPool.delete(db.events); -} - -async function createEvent(dbPool: NodePgDatabase, eventData: EventData[]) { - const events = []; - for (const event of eventData) { - const result = await dbPool - .insert(db.events) - .values({ - name: event.name, - fields: event.fields, - }) - .returning(); - events.push(result[0]); - } - return events; -} - -async function createCycle(dbPool: NodePgDatabase, cycleData: CycleData[]) { - if (cycleData.length === 0) { - throw new Error('Cycle data is empty.'); - } - - const cycles = []; - for (const cycle of cycleData) { - if (!cycle.eventId) { - throw new Error('Event ID is not defined.'); - } - - const result = await dbPool - .insert(db.cycles) - .values({ - startAt: cycle.startAt, - endAt: cycle.endAt, - status: cycle.status, - eventId: cycle.eventId, - }) - .returning(); - - cycles.push(result[0]); - } - - return cycles; -} - -async function createRegistrationFields( - dbPool: NodePgDatabase, - registrationFieldData: RegistrationFieldData[], -) { - if (registrationFieldData.length === 0) { - throw new Error('Registration field data is empty.'); - } - - const registrationFields = []; - for (const field of registrationFieldData) { - if (!field.eventId) { - throw new Error('Event ID is not defined for a registration field.'); - } - - const result = await dbPool - .insert(db.registrationFields) - .values({ - name: field.name, - type: field.type, - required: field.required, - forUser: field.forUser, - forGroup: field.forGroup, - eventId: field.eventId, - }) - .returning(); - - registrationFields.push(result[0]); - } - - return registrationFields; -} - -async function createRegistrationFieldOptions( - dbPool: NodePgDatabase, - registrationFieldOptionsData: RegistrationFieldOptionData[], -) { - if (registrationFieldOptionsData.length === 0) { - throw new Error('Registration Field Options data is empty.'); - } - - const registrationFieldOptions = []; - for (const optionData of registrationFieldOptionsData) { - if (!optionData.registrationFieldId) { - throw new Error('Registration Field id is not defined for a registration option.'); - } - - const result = await dbPool - .insert(db.registrationFieldOptions) - .values({ - registrationFieldId: optionData.registrationFieldId, - value: optionData.value, - }) - .returning(); - - registrationFieldOptions.push(result[0]); - } - - return registrationFieldOptions; -} - -async function createForumQuestions( - dbPool: NodePgDatabase, - forumQuestionData: ForumQuestionData[], -) { - if (forumQuestionData.length === 0) { - throw new Error('Forum Question data is empty.'); - } - - const forumQuestions = []; - for (const questionData of forumQuestionData) { - if (!questionData.cycleId) { - throw new Error('Cycle ID is not defined for the forum question.'); - } - - const result = await dbPool - .insert(db.questions) - .values({ - cycleId: questionData.cycleId, - title: questionData.title, - voteModel: questionData.voteModel, - }) - .returning(); - - forumQuestions.push(result[0]); - } - - return forumQuestions; -} - -async function createQuestionOptions( - dbPool: NodePgDatabase, - questionOptionData: QuestionOptionData[], -) { - if (questionOptionData.length === 0) { - throw new Error('Question Option data is empty.'); - } - - const questionOptions = []; - for (const questionOption of questionOptionData) { - if (!questionOption.questionId) { - throw new Error('Question ID is not defined for the question option.'); - } - - const result = await dbPool - .insert(db.options) - .values({ - questionId: questionOption.questionId, - title: questionOption.title, - show: questionOption.show, - }) - .returning(); - - questionOptions.push(result[0]); - } - - return questionOptions; -} - -async function createGroupCategories( - dbPool: NodePgDatabase, - groupCategoriesData: GroupCategoryData[], -) { - if (groupCategoriesData.length === 0) { - throw new Error('Group Categories data is empty.'); - } - - const groupCategories = []; - for (const data of groupCategoriesData) { - if (!data.eventId) { - throw new Error('Event ID is not defined for the group category.'); - } - - const result = await dbPool - .insert(db.groupCategories) - .values({ - name: data.name, - eventId: data.eventId, - userCanCreate: data.userCanCreate, - userCanView: data.userCanView, - required: data.required, - }) - .returning(); - - groupCategories.push(result[0]); - } - - return groupCategories; -} - -async function createGroups(dbPool: NodePgDatabase, groupData: GroupData[]) { - if (groupData.length === 0) { - throw new Error('Group Data is empty.'); - } - - const groups = []; - for (const group of groupData) { - if (!group.groupCategoryId) { - throw new Error('Group Category ID is not defined for the group.'); - } - - const result = await dbPool - .insert(db.groups) - .values({ - name: group.name, - groupCategoryId: group.groupCategoryId, - }) - .returning(); - - groups.push(result[0]); - } - - return groups; -} - -async function createUsers(dbPool: NodePgDatabase, userData: UserData[]) { - const users = []; - for (const user of userData) { - const result = await dbPool - .insert(db.users) - .values({ - username: user.username, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - }) - .returning(); - - users.push(result[0]); - } - - return users; -} - -async function createUsersToGroups( - dbPool: NodePgDatabase, - usersToGroupsData: UsersToGroupsData[], -) { - if (usersToGroupsData.length === 0) { - throw new Error('Users to Groups Data is empty.'); - } - - const usersToGroups = []; - for (const group of usersToGroupsData) { - if (!group.groupId) { - throw new Error('Group ID is not defined for the users to groups relationship.'); - } - - const result = await dbPool - .insert(db.usersToGroups) - .values({ - userId: group.userId, - groupId: group.groupId, - groupCategoryId: group.groupCategoryId, - }) - .returning(); - - usersToGroups.push(result[0]); - } - - return usersToGroups; -} - -async function createQuestionsToGroupCategories( - dbPool: NodePgDatabase, - questionsToGroupCategoriesData: QuestionsToGroupCategoriesData[], -) { - if (questionsToGroupCategoriesData.length === 0) { - throw new Error('Questions to Group Categories Data is empty.'); - } - - const questionsToGroupCategories = []; - for (const groupCategories of questionsToGroupCategoriesData) { - if (!groupCategories.questionId) { - throw new Error('Question ID is not defined for the group Category.'); - } - - const result = await dbPool - .insert(db.questionsToGroupCategories) - .values({ - questionId: groupCategories.questionId, - groupCategoryId: groupCategories.groupCategoryId, - }) - .returning(); - - questionsToGroupCategories.push(result[0]); - } - - return questionsToGroupCategories; -} - -export { seed, cleanup }; diff --git a/src/utils/logger/index.ts b/src/utils/logger/index.ts index 07e128a0..2b14a06f 100644 --- a/src/utils/logger/index.ts +++ b/src/utils/logger/index.ts @@ -1,13 +1,15 @@ import pino from 'pino'; +import pretty from 'pino-pretty'; -// this requires pino-pretty to be installed as dev dep -export const logger = pino({ - transport: { - target: process.env.NODE_ENV === 'production' ? 'pino' : 'pino-pretty', - // FATAL, ERROR, WARN, INFO, DEBUG, TRACE +export const stream = pretty({ + sync: true, + colorize: true, +}); + +// have logger use pretty stream in dev mode, else just use default pino +export const logger = pino( + { level: process.env.LOG_LEVEL || 'info', - options: { - colorize: true, - }, }, -}); + process.env.NODE_ENV === 'production' ? undefined : stream, +); diff --git a/src/utils/db/mnemonics.ts b/src/utils/mnemonics.ts similarity index 100% rename from src/utils/db/mnemonics.ts rename to src/utils/mnemonics.ts From 6296395c19067ca8aeae8bb4cbb5f75d179f6d95 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Thu, 25 Jul 2024 10:52:52 +0100 Subject: [PATCH 11/14] update fields structure to dict (#450) * update validation schema * update fields object to be dict and not array * fix tests --- migrations/0028_chilly_sentry.sql | 13 + migrations/0028_green_gamora.sql | 5 - migrations/0029_quick_ares.sql | 7 - migrations/0030_polite_forge.sql | 1 - migrations/meta/0028_snapshot.json | 34 +- migrations/meta/0029_snapshot.json | 1699 ---------------------------- migrations/meta/0030_snapshot.json | 1692 --------------------------- migrations/meta/_journal.json | 18 +- src/db/schema/events.ts | 2 +- src/db/schema/questions.ts | 2 +- src/db/seed.ts | 9 +- src/services/validation.spec.ts | 94 +- src/services/validation.ts | 2 +- src/types/validation.ts | 31 +- 14 files changed, 121 insertions(+), 3488 deletions(-) create mode 100644 migrations/0028_chilly_sentry.sql delete mode 100644 migrations/0028_green_gamora.sql delete mode 100644 migrations/0029_quick_ares.sql delete mode 100644 migrations/0030_polite_forge.sql delete mode 100644 migrations/meta/0029_snapshot.json delete mode 100644 migrations/meta/0030_snapshot.json diff --git a/migrations/0028_chilly_sentry.sql b/migrations/0028_chilly_sentry.sql new file mode 100644 index 00000000..b103298f --- /dev/null +++ b/migrations/0028_chilly_sentry.sql @@ -0,0 +1,13 @@ +ALTER TABLE "options" RENAME COLUMN "accepted" TO "show";--> statement-breakpoint +ALTER TABLE "questions_to_group_categories" ALTER COLUMN "group_category_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "events" ADD COLUMN "fields" jsonb DEFAULT '{}'::jsonb NOT NULL;--> statement-breakpoint +ALTER TABLE "registrations" ADD COLUMN "data" jsonb;--> statement-breakpoint +ALTER TABLE "questions" ADD COLUMN "user_can_create" boolean DEFAULT false;--> statement-breakpoint +ALTER TABLE "questions" ADD COLUMN "fields" jsonb DEFAULT '{}'::jsonb NOT NULL;--> statement-breakpoint +ALTER TABLE "options" ADD COLUMN "group_id" uuid;--> statement-breakpoint +ALTER TABLE "options" ADD COLUMN "data" jsonb;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "options" ADD CONSTRAINT "options_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/migrations/0028_green_gamora.sql b/migrations/0028_green_gamora.sql deleted file mode 100644 index 00a23f02..00000000 --- a/migrations/0028_green_gamora.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE "options" RENAME COLUMN "accepted" TO "show";--> statement-breakpoint -ALTER TABLE "events" ADD COLUMN "fields" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint -ALTER TABLE "registrations" ADD COLUMN "data" jsonb;--> statement-breakpoint -ALTER TABLE "questions" ADD COLUMN "fields" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint -ALTER TABLE "options" ADD COLUMN "data" jsonb; \ No newline at end of file diff --git a/migrations/0029_quick_ares.sql b/migrations/0029_quick_ares.sql deleted file mode 100644 index e81272c3..00000000 --- a/migrations/0029_quick_ares.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE "questions" ADD COLUMN "user_can_create" boolean DEFAULT false;--> statement-breakpoint -ALTER TABLE "options" ADD COLUMN "group_id" uuid;--> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "options" ADD CONSTRAINT "options_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; diff --git a/migrations/0030_polite_forge.sql b/migrations/0030_polite_forge.sql deleted file mode 100644 index 5f4c0238..00000000 --- a/migrations/0030_polite_forge.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "questions_to_group_categories" ALTER COLUMN "group_category_id" SET NOT NULL; \ No newline at end of file diff --git a/migrations/meta/0028_snapshot.json b/migrations/meta/0028_snapshot.json index a199029b..12108408 100644 --- a/migrations/meta/0028_snapshot.json +++ b/migrations/meta/0028_snapshot.json @@ -1,5 +1,5 @@ { - "id": "3b3f3fe7-aab6-4ea0-88cd-e1c81a678986", + "id": "f5176ae8-f870-4d8c-82dc-550ae61ecb56", "prevId": "201665be-855d-4af0-8e69-731c400b4907", "version": "6", "dialect": "postgresql", @@ -265,7 +265,7 @@ "type": "jsonb", "primaryKey": false, "notNull": true, - "default": "'[]'::jsonb" + "default": "'{}'::jsonb" }, "image_url": { "name": "image_url", @@ -754,12 +754,19 @@ "notNull": false, "default": false }, + "user_can_create": { + "name": "user_can_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, "fields": { "name": "fields", "type": "jsonb", "primaryKey": false, "notNull": true, - "default": "'[]'::jsonb" + "default": "'{}'::jsonb" }, "created_at": { "name": "created_at", @@ -875,6 +882,12 @@ "primaryKey": false, "notNull": false }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "question_id": { "name": "question_id", "type": "uuid", @@ -963,6 +976,19 @@ "onDelete": "no action", "onUpdate": "no action" }, + "options_group_id_groups_id_fk": { + "name": "options_group_id_groups_id_fk", + "tableFrom": "options", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, "options_question_id_questions_id_fk": { "name": "options_question_id_questions_id_fk", "tableFrom": "options", @@ -1613,7 +1639,7 @@ "name": "group_category_id", "type": "uuid", "primaryKey": false, - "notNull": false + "notNull": true }, "created_at": { "name": "created_at", diff --git a/migrations/meta/0029_snapshot.json b/migrations/meta/0029_snapshot.json deleted file mode 100644 index 250a4c47..00000000 --- a/migrations/meta/0029_snapshot.json +++ /dev/null @@ -1,1699 +0,0 @@ -{ - "id": "f83795a1-f552-488f-b670-bd7c417424b8", - "prevId": "3b3f3fe7-aab6-4ea0-88cd-e1c81a678986", - "version": "6", - "dialect": "postgresql", - "tables": { - "public.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": {} - }, - "public.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 - }, - "option_id": { - "name": "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_option_id_options_id_fk": { - "name": "comments_option_id_options_id_fk", - "tableFrom": "comments", - "tableTo": "options", - "columnsFrom": [ - "option_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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": {} - }, - "public.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 - }, - "require_approval": { - "name": "require_approval", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "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 - }, - "fields": { - "name": "fields", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "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": {} - }, - "public.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" - ] - } - } - }, - "public.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 - }, - "required": { - "name": "required", - "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": {} - }, - "public.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": 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": { - "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" - ] - } - } - }, - "public.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" - ] - } - } - }, - "public.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'" - }, - "data": { - "name": "data", - "type": "jsonb", - "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": { - "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": {} - }, - "public.questions": { - "name": "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 - }, - "title": { - "name": "title", - "type": "varchar(256)", - "primaryKey": false, - "notNull": true - }, - "sub_title": { - "name": "sub_title", - "type": "varchar(256)", - "primaryKey": false, - "notNull": false - }, - "vote_model": { - "name": "vote_model", - "type": "varchar(256)", - "primaryKey": false, - "notNull": true, - "default": "'COCM'" - }, - "show_score": { - "name": "show_score", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "fields": { - "name": "fields", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "user_can_create": { - "name": "user_can_create", - "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": { - "questions_cycle_id_cycles_id_fk": { - "name": "questions_cycle_id_cycles_id_fk", - "tableFrom": "questions", - "tableTo": "cycles", - "columnsFrom": [ - "cycle_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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": {} - }, - "public.options": { - "name": "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 - }, - "group_id": { - "name": "group_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "question_id": { - "name": "question_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "varchar(256)", - "primaryKey": false, - "notNull": true - }, - "sub_title": { - "name": "sub_title", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "show": { - "name": "show", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "vote_score": { - "name": "vote_score", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0.0'" - }, - "funding_request": { - "name": "funding_request", - "type": "numeric", - "primaryKey": false, - "notNull": false, - "default": "'0.0'" - }, - "data": { - "name": "data", - "type": "jsonb", - "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": { - "options_user_id_users_id_fk": { - "name": "options_user_id_users_id_fk", - "tableFrom": "options", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "options_registration_id_registrations_id_fk": { - "name": "options_registration_id_registrations_id_fk", - "tableFrom": "options", - "tableTo": "registrations", - "columnsFrom": [ - "registration_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "options_group_id_groups_id_fk": { - "name": "options_group_id_groups_id_fk", - "tableFrom": "options", - "tableTo": "groups", - "columnsFrom": [ - "group_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "options_question_id_questions_id_fk": { - "name": "options_question_id_questions_id_fk", - "tableFrom": "options", - "tableTo": "questions", - "columnsFrom": [ - "question_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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_options_id_fk": { - "name": "votes_option_id_options_id_fk", - "tableFrom": "votes", - "tableTo": "options", - "columnsFrom": [ - "option_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "votes_question_id_questions_id_fk": { - "name": "votes_question_id_questions_id_fk", - "tableFrom": "votes", - "tableTo": "questions", - "columnsFrom": [ - "question_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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 - }, - "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" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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": {} - }, - "public.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": {} - }, - "public.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": {} - }, - "public.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": {} - }, - "public.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" - ] - } - } - }, - "public.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": {} - }, - "public.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_questions_id_fk": { - "name": "questions_to_group_categories_question_id_questions_id_fk", - "tableFrom": "questions_to_group_categories", - "tableTo": "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/0030_snapshot.json b/migrations/meta/0030_snapshot.json deleted file mode 100644 index aba98eb5..00000000 --- a/migrations/meta/0030_snapshot.json +++ /dev/null @@ -1,1692 +0,0 @@ -{ - "id": "02ccac96-115d-47d7-86c9-d6f3c50b3d06", - "prevId": "ce83aad2-50c3-4f21-9fd5-0d684d95e87c", - "version": "6", - "dialect": "postgresql", - "tables": { - "public.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": {} - }, - "public.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 - }, - "option_id": { - "name": "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_option_id_options_id_fk": { - "name": "comments_option_id_options_id_fk", - "tableFrom": "comments", - "tableTo": "options", - "columnsFrom": [ - "option_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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": {} - }, - "public.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 - }, - "require_approval": { - "name": "require_approval", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "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 - }, - "fields": { - "name": "fields", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "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": {} - }, - "public.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" - ] - } - } - }, - "public.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 - }, - "required": { - "name": "required", - "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": {} - }, - "public.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": 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": { - "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" - ] - } - } - }, - "public.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" - ] - } - } - }, - "public.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'" - }, - "data": { - "name": "data", - "type": "jsonb", - "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": { - "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": {} - }, - "public.questions": { - "name": "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 - }, - "title": { - "name": "title", - "type": "varchar(256)", - "primaryKey": false, - "notNull": true - }, - "sub_title": { - "name": "sub_title", - "type": "varchar(256)", - "primaryKey": false, - "notNull": false - }, - "vote_model": { - "name": "vote_model", - "type": "varchar(256)", - "primaryKey": false, - "notNull": true, - "default": "'COCM'" - }, - "show_score": { - "name": "show_score", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "fields": { - "name": "fields", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "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_cycle_id_cycles_id_fk": { - "name": "questions_cycle_id_cycles_id_fk", - "tableFrom": "questions", - "tableTo": "cycles", - "columnsFrom": [ - "cycle_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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": {} - }, - "public.options": { - "name": "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 - }, - "group_id": { - "name": "group_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "question_id": { - "name": "question_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "varchar(256)", - "primaryKey": false, - "notNull": true - }, - "sub_title": { - "name": "sub_title", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "show": { - "name": "show", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "vote_score": { - "name": "vote_score", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0.0'" - }, - "funding_request": { - "name": "funding_request", - "type": "numeric", - "primaryKey": false, - "notNull": false, - "default": "'0.0'" - }, - "data": { - "name": "data", - "type": "jsonb", - "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": { - "options_user_id_users_id_fk": { - "name": "options_user_id_users_id_fk", - "tableFrom": "options", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "options_registration_id_registrations_id_fk": { - "name": "options_registration_id_registrations_id_fk", - "tableFrom": "options", - "tableTo": "registrations", - "columnsFrom": [ - "registration_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "options_group_id_groups_id_fk": { - "name": "options_group_id_groups_id_fk", - "tableFrom": "options", - "tableTo": "groups", - "columnsFrom": [ - "group_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "options_question_id_questions_id_fk": { - "name": "options_question_id_questions_id_fk", - "tableFrom": "options", - "tableTo": "questions", - "columnsFrom": [ - "question_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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_options_id_fk": { - "name": "votes_option_id_options_id_fk", - "tableFrom": "votes", - "tableTo": "options", - "columnsFrom": [ - "option_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "votes_question_id_questions_id_fk": { - "name": "votes_question_id_questions_id_fk", - "tableFrom": "votes", - "tableTo": "questions", - "columnsFrom": [ - "question_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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 - }, - "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" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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": {} - }, - "public.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": {} - }, - "public.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": {} - }, - "public.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": {} - }, - "public.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" - ] - } - } - }, - "public.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": {} - }, - "public.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": 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": { - "questions_to_group_categories_question_id_questions_id_fk": { - "name": "questions_to_group_categories_question_id_questions_id_fk", - "tableFrom": "questions_to_group_categories", - "tableTo": "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 1ab4fe2a..963d2fe4 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -201,22 +201,8 @@ { "idx": 28, "version": "6", - "when": 1720187374529, - "tag": "0028_green_gamora", - "breakpoints": true - }, - { - "idx": 29, - "version": "6", - "when": 1720519200977, - "tag": "0029_quick_ares", - "breakpoints": true - }, - { - "idx": 30, - "version": "6", - "when": 1720536020805, - "tag": "0030_polite_forge", + "when": 1721839965156, + "tag": "0028_chilly_sentry", "breakpoints": true } ] diff --git a/src/db/schema/events.ts b/src/db/schema/events.ts index 08076458..3e385dc8 100644 --- a/src/db/schema/events.ts +++ b/src/db/schema/events.ts @@ -12,7 +12,7 @@ export const events = pgTable('events', { description: varchar('description'), link: varchar('link'), registrationDescription: varchar('registration_description'), - fields: jsonb('fields').notNull().default([]), + fields: jsonb('fields').notNull().default({}), imageUrl: varchar('image_url'), eventDisplayRank: integer('event_display_rank'), createdAt: timestamp('created_at').notNull().defaultNow(), diff --git a/src/db/schema/questions.ts b/src/db/schema/questions.ts index 92a5f1a5..0817484b 100644 --- a/src/db/schema/questions.ts +++ b/src/db/schema/questions.ts @@ -13,8 +13,8 @@ export const questions = pgTable('questions', { subTitle: varchar('sub_title', { length: 256 }), voteModel: varchar('vote_model', { length: 256 }).notNull().default('COCM'), showScore: boolean('show_score').default(false), - fields: jsonb('fields').notNull().default([]), userCanCreate: boolean('user_can_create').default(false), + fields: jsonb('fields').notNull().default({}), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); diff --git a/src/db/seed.ts b/src/db/seed.ts index a812a194..9b5ab97e 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -33,12 +33,13 @@ const insertUsersSchema = createInsertSchema(schema.users); const insertUsersToGroupsSchema = createInsertSchema(schema.usersToGroups); async function seed(dbPool: NodePgDatabase) { + const randId = randUuid(); const events = await createEvent(dbPool, [ { name: randCity(), - fields: [ - { - id: randUuid(), + fields: { + [randId]: { + id: randId, name: 'submit project', type: 'TEXT', position: 0, @@ -46,7 +47,7 @@ async function seed(dbPool: NodePgDatabase) { required: true, }, }, - ], + }, }, ]); const cycles = await createCycle(dbPool, [ diff --git a/src/services/validation.spec.ts b/src/services/validation.spec.ts index 347140d1..5d44f0c8 100644 --- a/src/services/validation.spec.ts +++ b/src/services/validation.spec.ts @@ -7,8 +7,8 @@ import assert from 'node:assert/strict'; describe('service: validation', function () { describe('rule: required', function () { test('should return an error if a required field is missing', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + name: { id: 'name', name: 'Name', position: 1, @@ -17,7 +17,7 @@ describe('service: validation', function () { required: true, }, }, - ]; + }; const data: z.infer = {}; const result = enforceRules({ data, fields }); @@ -27,8 +27,8 @@ describe('service: validation', function () { }); test('should not return an error if a required field is present', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + name: { id: 'name', name: 'Name', position: 1, @@ -37,7 +37,8 @@ describe('service: validation', function () { required: true, }, }, - ]; + }; + const data: z.infer = { name: { value: 'John Doe', @@ -55,8 +56,8 @@ describe('service: validation', function () { describe('officer: string', function () { describe('rule: minLength', function () { test('should return an error if the string is too short', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + name: { id: 'name', name: 'Name', position: 1, @@ -66,7 +67,8 @@ describe('service: validation', function () { minLength: 5, }, }, - ]; + }; + const data: z.infer = { name: { value: 'John', @@ -82,8 +84,8 @@ describe('service: validation', function () { }); test('should not return an error if the string is long enough', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + name: { id: 'name', name: 'Name', position: 1, @@ -93,7 +95,8 @@ describe('service: validation', function () { minLength: 5, }, }, - ]; + }; + const data: z.infer = { name: { value: 'John Doe', @@ -109,8 +112,8 @@ describe('service: validation', function () { }); describe('rule: maxLength', function () { test('should return an error if the string is too long', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + name: { id: 'name', name: 'Name', position: 1, @@ -120,7 +123,8 @@ describe('service: validation', function () { maxLength: 5, }, }, - ]; + }; + const data: z.infer = { name: { value: 'John Doe', @@ -136,8 +140,8 @@ describe('service: validation', function () { }); test('should not return an error if the string is short enough', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + name: { id: 'name', name: 'Name', position: 1, @@ -147,7 +151,8 @@ describe('service: validation', function () { maxLength: 5, }, }, - ]; + }; + const data: z.infer = { name: { value: 'John', @@ -166,8 +171,8 @@ describe('service: validation', function () { describe('officer: number', function () { describe('rule: minLength', function () { test('should return an error if the number is too small', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + age: { id: 'age', name: 'Age', position: 1, @@ -177,7 +182,7 @@ describe('service: validation', function () { minLength: 18, }, }, - ]; + }; const data: z.infer = { age: { value: 17, @@ -193,8 +198,8 @@ describe('service: validation', function () { }); test('should not return an error if the number is large enough', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + age: { id: 'age', name: 'Age', position: 1, @@ -204,7 +209,8 @@ describe('service: validation', function () { minLength: 18, }, }, - ]; + }; + const data: z.infer = { age: { value: 18, @@ -220,8 +226,8 @@ describe('service: validation', function () { }); describe('rule: maxLength', function () { test('should return an error if the number is too large', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + age: { id: 'age', name: 'Age', position: 1, @@ -231,7 +237,8 @@ describe('service: validation', function () { maxLength: 18, }, }, - ]; + }; + const data: z.infer = { age: { value: 19, @@ -246,8 +253,8 @@ describe('service: validation', function () { assert.deepEqual(result, ['Age must be at most 18']); }); test('should not return an error if the number is small enough', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + age: { id: 'age', name: 'Age', position: 1, @@ -257,7 +264,8 @@ describe('service: validation', function () { maxLength: 18, }, }, - ]; + }; + const data: z.infer = { age: { value: 18, @@ -276,8 +284,8 @@ describe('service: validation', function () { describe('officer: array', function () { describe('rule: minLength', function () { test('should return an error if the array is too small', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + colors: { id: 'colors', name: 'Colors', position: 1, @@ -287,7 +295,8 @@ describe('service: validation', function () { minLength: 2, }, }, - ]; + }; + const data: z.infer = { colors: { value: ['red'], @@ -302,8 +311,8 @@ describe('service: validation', function () { assert.deepEqual(result, ['Colors must have at least 2 items']); }); test('should not return an error if the array is large enough', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + colors: { id: 'colors', name: 'Colors', position: 1, @@ -313,7 +322,8 @@ describe('service: validation', function () { minLength: 2, }, }, - ]; + }; + const data: z.infer = { colors: { value: ['red', 'blue'], @@ -329,8 +339,8 @@ describe('service: validation', function () { }); describe('rule: maxLength', function () { test('should return an error if the array is too large', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + colors: { id: 'colors', name: 'Colors', position: 1, @@ -340,7 +350,7 @@ describe('service: validation', function () { maxLength: 2, }, }, - ]; + }; const data: z.infer = { colors: { value: ['red', 'blue', 'green'], @@ -355,8 +365,8 @@ describe('service: validation', function () { assert.deepEqual(result, ['Colors must have at most 2 items']); }); test('should not return an error if the array is small enough', function () { - const fields: z.infer = [ - { + const fields: z.infer = { + colors: { id: 'colors', name: 'Colors', position: 1, @@ -366,7 +376,7 @@ describe('service: validation', function () { maxLength: 2, }, }, - ]; + }; const data: z.infer = { colors: { value: ['red', 'blue'], diff --git a/src/services/validation.ts b/src/services/validation.ts index 3e74d9fe..415204c1 100644 --- a/src/services/validation.ts +++ b/src/services/validation.ts @@ -16,7 +16,7 @@ export function enforceRules({ return []; } - for (const field of fields) { + for (const field of Object.values(fields)) { const value = data?.[field.id]?.value; if (field.validation.required && !value) { diff --git a/src/types/validation.ts b/src/types/validation.ts index 8107c4b5..5a0b47fa 100644 --- a/src/types/validation.ts +++ b/src/types/validation.ts @@ -6,21 +6,22 @@ import { z } from 'zod'; const fieldType = z.enum(['TEXT', 'TEXTAREA', 'SELECT', 'CHECKBOX', 'MULTI_SELECT', 'NUMBER']); -export const fieldsSchema = z.array( - z.object({ - id: z.string().uuid(), - name: z.string(), - description: z.string().optional(), - type: fieldType, - position: z.number(), - options: z.array(z.string()).optional(), - validation: z.object({ - required: z.boolean(), - minLength: z.number().optional(), - maxLength: z.number().optional(), - }), +export const fieldSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().optional(), + type: fieldType, + position: z.coerce.number(), + options: z.array(z.string()).optional(), + validation: z.object({ + required: z.boolean(), + minLength: z.coerce.number().optional().nullable(), + maxLength: z.coerce.number().optional().nullable(), }), -); +}); + +// [fieldId] => { ...fieldSchema } +export const fieldsSchema = z.record(z.string().uuid(), fieldSchema); /** * Data Schema @@ -42,4 +43,4 @@ const dataForOneFieldSchema = z.object({ }); // [fieldId] => { value: [value], fieldId: [fieldId] } -export const dataSchema = z.record(z.string(), dataForOneFieldSchema); +export const dataSchema = z.record(z.string().uuid(), dataForOneFieldSchema); From bcdcfff78551f6decfc47ff68cf0b90090412bd2 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Fri, 26 Jul 2024 10:03:11 +0100 Subject: [PATCH 12/14] add registration to seed (#451) * add registration to seed * Update src/db/seed.ts Co-authored-by: Martin Benedikt Busch <43137759+MartinBenediktBusch@users.noreply.github.com> --------- Co-authored-by: Martin Benedikt Busch <43137759+MartinBenediktBusch@users.noreply.github.com> --- src/db/seed.ts | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/db/seed.ts b/src/db/seed.ts index 9b5ab97e..24b1b96d 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -7,13 +7,14 @@ import { randFirstName, randJobTitle, randLastName, + randNumber, randUserName, randUuid, } from '@ngneat/falso'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { createInsertSchema } from 'drizzle-zod'; import { z } from 'zod'; -import { fieldsSchema, insertOptionsSchema } from '../types'; +import { dataSchema, fieldsSchema, insertOptionsSchema } from '../types'; import * as schema from './schema'; // Define the data types for the seed function @@ -33,20 +34,30 @@ const insertUsersSchema = createInsertSchema(schema.users); const insertUsersToGroupsSchema = createInsertSchema(schema.usersToGroups); async function seed(dbPool: NodePgDatabase) { - const randId = randUuid(); + const randCityFieldId = randUuid(); + const randAgeFieldId = randUuid(); const events = await createEvent(dbPool, [ { name: randCity(), fields: { - [randId]: { - id: randId, - name: 'submit project', + [randCityFieldId]: { + id: randCityFieldId, + name: 'city', type: 'TEXT', position: 0, validation: { required: true, }, }, + [randAgeFieldId]: { + id: randAgeFieldId, + name: 'age', + type: 'NUMBER', + position: 1, + validation: { + required: true, + }, + }, }, }, ]); @@ -241,6 +252,28 @@ async function seed(dbPool: NodePgDatabase) { }, ]); + const registration: z.infer = { + [randCityFieldId]: { + fieldId: randCityFieldId, + value: randCity(), + type: 'TEXT', + }, + [randAgeFieldId]: { + fieldId: randAgeFieldId, + value: randNumber({ min: 18, max: 99 }), + type: 'NUMBER', + }, + }; + + await dbPool + .insert(schema.registrations) + .values({ + eventId: events[0]!.id, + userId: users[0]!.id, + data: registration, + }) + .returning(); + return { events, cycles, From 75ff49efc35d1c00cfe23ceb6d5fb1d7cf66ef89 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Tue, 30 Jul 2024 15:12:52 +0100 Subject: [PATCH 13/14] add status to events (#452) * add status to events * evaluate upcoming before closed --- migrations/0029_outgoing_true_believers.sql | 1 + migrations/meta/0029_snapshot.json | 1707 +++++++++++++++++++ migrations/meta/_journal.json | 7 + src/db/schema/cycles.ts | 32 +- src/handlers/events.ts | 24 +- 5 files changed, 1756 insertions(+), 15 deletions(-) create mode 100644 migrations/0029_outgoing_true_believers.sql create mode 100644 migrations/meta/0029_snapshot.json diff --git a/migrations/0029_outgoing_true_believers.sql b/migrations/0029_outgoing_true_believers.sql new file mode 100644 index 00000000..d8842df2 --- /dev/null +++ b/migrations/0029_outgoing_true_believers.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS "status_idx" ON "cycles" ("status"); \ No newline at end of file diff --git a/migrations/meta/0029_snapshot.json b/migrations/meta/0029_snapshot.json new file mode 100644 index 00000000..b204ba23 --- /dev/null +++ b/migrations/meta/0029_snapshot.json @@ -0,0 +1,1707 @@ +{ + "id": "abf3f64f-caa8-490e-9457-05c8eef1cda4", + "prevId": "f5176ae8-f870-4d8c-82dc-550ae61ecb56", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.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": {} + }, + "public.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 + }, + "option_id": { + "name": "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_option_id_options_id_fk": { + "name": "comments_option_id_options_id_fk", + "tableFrom": "comments", + "tableTo": "options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": { + "status_idx": { + "name": "status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "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": {} + }, + "public.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 + }, + "require_approval": { + "name": "require_approval", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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 + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": {} + }, + "public.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" + ] + } + } + }, + "public.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 + }, + "required": { + "name": "required", + "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": {} + }, + "public.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": 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": { + "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" + ] + } + } + }, + "public.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" + ] + } + } + }, + "public.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'" + }, + "data": { + "name": "data", + "type": "jsonb", + "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": { + "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": {} + }, + "public.questions": { + "name": "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 + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "sub_title": { + "name": "sub_title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "vote_model": { + "name": "vote_model", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "default": "'COCM'" + }, + "show_score": { + "name": "show_score", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_can_create": { + "name": "user_can_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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_cycle_id_cycles_id_fk": { + "name": "questions_cycle_id_cycles_id_fk", + "tableFrom": "questions", + "tableTo": "cycles", + "columnsFrom": [ + "cycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.options": { + "name": "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 + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "sub_title": { + "name": "sub_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "show": { + "name": "show", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vote_score": { + "name": "vote_score", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0.0'" + }, + "funding_request": { + "name": "funding_request", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0.0'" + }, + "data": { + "name": "data", + "type": "jsonb", + "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": { + "options_user_id_users_id_fk": { + "name": "options_user_id_users_id_fk", + "tableFrom": "options", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_registration_id_registrations_id_fk": { + "name": "options_registration_id_registrations_id_fk", + "tableFrom": "options", + "tableTo": "registrations", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_group_id_groups_id_fk": { + "name": "options_group_id_groups_id_fk", + "tableFrom": "options", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "options_question_id_questions_id_fk": { + "name": "options_question_id_questions_id_fk", + "tableFrom": "options", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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_options_id_fk": { + "name": "votes_option_id_options_id_fk", + "tableFrom": "votes", + "tableTo": "options", + "columnsFrom": [ + "option_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "votes_question_id_questions_id_fk": { + "name": "votes_question_id_questions_id_fk", + "tableFrom": "votes", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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 + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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": {} + }, + "public.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" + ] + } + } + }, + "public.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": {} + }, + "public.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": 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": { + "questions_to_group_categories_question_id_questions_id_fk": { + "name": "questions_to_group_categories_question_id_questions_id_fk", + "tableFrom": "questions_to_group_categories", + "tableTo": "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 963d2fe4..f135dff4 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -204,6 +204,13 @@ "when": 1721839965156, "tag": "0028_chilly_sentry", "breakpoints": true + }, + { + "idx": 29, + "version": "6", + "when": 1722348223783, + "tag": "0029_outgoing_true_believers", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/cycles.ts b/src/db/schema/cycles.ts index 834c0e01..a911106f 100644 --- a/src/db/schema/cycles.ts +++ b/src/db/schema/cycles.ts @@ -1,20 +1,26 @@ import { relations } from 'drizzle-orm'; -import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; +import { index, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import { questions } from './questions'; import { events } from './events'; -export const cycles = pgTable('cycles', { - id: uuid('id').primaryKey().defaultRandom(), - eventId: uuid('event_id').references(() => events.id), - startAt: timestamp('start_at').notNull(), - endAt: timestamp('end_at').notNull(), - // OPEN / CLOSED / UPCOMING - status: varchar('status', { - length: 20, - }).default('UPCOMING'), - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at').notNull().defaultNow(), -}); +export const cycles = pgTable( + 'cycles', + { + id: uuid('id').primaryKey().defaultRandom(), + eventId: uuid('event_id').references(() => events.id), + startAt: timestamp('start_at').notNull(), + endAt: timestamp('end_at').notNull(), + // OPEN / CLOSED / UPCOMING + status: varchar('status', { + length: 20, + }).default('UPCOMING'), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (t) => ({ + statusIndex: index('status_idx').on(t.status), + }), +); export const cyclesRelations = relations(cycles, ({ many, one }) => ({ questions: many(questions), diff --git a/src/handlers/events.ts b/src/handlers/events.ts index e58f05b1..0f1e3883 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -1,4 +1,4 @@ -import { and, eq } from 'drizzle-orm'; +import { and, eq, sql } from 'drizzle-orm'; import type { Request, Response } from 'express'; import * as schema from '../db/schema'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; @@ -50,7 +50,27 @@ export function getEventGroupCategoriesHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { - const events = await dbPool.query.events.findMany(); + const events = await dbPool.query.events.findMany({ + extras: { + status: sql` + CASE + WHEN EXISTS ( + SELECT 1 + FROM ${schema.cycles} + WHERE ${schema.cycles.eventId} = ${schema.events.id} + AND ${schema.cycles.status} = 'OPEN' + ) THEN 'OPEN' + WHEN EXISTS ( + SELECT 1 + FROM ${schema.cycles} + WHERE ${schema.cycles.eventId} = ${schema.events.id} + AND ${schema.cycles.status} = 'UPCOMING' + ) THEN 'UPCOMING' + ELSE 'CLOSED' + END + `.as('status'), + }, + }); return res.json({ data: events }); }; } From 81b3762363b25dcb534d5865e313845bdf9b9cf4 Mon Sep 17 00:00:00 2001 From: Diego Alzate Date: Tue, 30 Jul 2024 15:36:50 +0100 Subject: [PATCH 14/14] consolidate migrations --- ...lly_sentry.sql => 0028_keen_nick_fury.sql} | 2 + migrations/0029_outgoing_true_believers.sql | 1 - migrations/meta/0028_snapshot.json | 12 +- migrations/meta/0029_snapshot.json | 1707 ----------------- migrations/meta/_journal.json | 11 +- 5 files changed, 14 insertions(+), 1719 deletions(-) rename migrations/{0028_chilly_sentry.sql => 0028_keen_nick_fury.sql} (91%) delete mode 100644 migrations/0029_outgoing_true_believers.sql delete mode 100644 migrations/meta/0029_snapshot.json diff --git a/migrations/0028_chilly_sentry.sql b/migrations/0028_keen_nick_fury.sql similarity index 91% rename from migrations/0028_chilly_sentry.sql rename to migrations/0028_keen_nick_fury.sql index b103298f..6d469850 100644 --- a/migrations/0028_chilly_sentry.sql +++ b/migrations/0028_keen_nick_fury.sql @@ -11,3 +11,5 @@ DO $$ BEGIN EXCEPTION WHEN duplicate_object THEN null; END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "status_idx" ON "cycles" ("status"); \ No newline at end of file diff --git a/migrations/0029_outgoing_true_believers.sql b/migrations/0029_outgoing_true_believers.sql deleted file mode 100644 index d8842df2..00000000 --- a/migrations/0029_outgoing_true_believers.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX IF NOT EXISTS "status_idx" ON "cycles" ("status"); \ No newline at end of file diff --git a/migrations/meta/0028_snapshot.json b/migrations/meta/0028_snapshot.json index 12108408..f7bb946c 100644 --- a/migrations/meta/0028_snapshot.json +++ b/migrations/meta/0028_snapshot.json @@ -1,5 +1,5 @@ { - "id": "f5176ae8-f870-4d8c-82dc-550ae61ecb56", + "id": "38b232a4-f172-4406-8b6a-bcd48ee6c5b0", "prevId": "201665be-855d-4af0-8e69-731c400b4907", "version": "6", "dialect": "postgresql", @@ -199,7 +199,15 @@ "default": "now()" } }, - "indexes": {}, + "indexes": { + "status_idx": { + "name": "status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, "foreignKeys": { "cycles_event_id_events_id_fk": { "name": "cycles_event_id_events_id_fk", diff --git a/migrations/meta/0029_snapshot.json b/migrations/meta/0029_snapshot.json deleted file mode 100644 index b204ba23..00000000 --- a/migrations/meta/0029_snapshot.json +++ /dev/null @@ -1,1707 +0,0 @@ -{ - "id": "abf3f64f-caa8-490e-9457-05c8eef1cda4", - "prevId": "f5176ae8-f870-4d8c-82dc-550ae61ecb56", - "version": "6", - "dialect": "postgresql", - "tables": { - "public.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": {} - }, - "public.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 - }, - "option_id": { - "name": "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_option_id_options_id_fk": { - "name": "comments_option_id_options_id_fk", - "tableFrom": "comments", - "tableTo": "options", - "columnsFrom": [ - "option_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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": { - "status_idx": { - "name": "status_idx", - "columns": [ - "status" - ], - "isUnique": false - } - }, - "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": {} - }, - "public.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 - }, - "require_approval": { - "name": "require_approval", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "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 - }, - "fields": { - "name": "fields", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - }, - "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": {} - }, - "public.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" - ] - } - } - }, - "public.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 - }, - "required": { - "name": "required", - "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": {} - }, - "public.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": 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": { - "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" - ] - } - } - }, - "public.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" - ] - } - } - }, - "public.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'" - }, - "data": { - "name": "data", - "type": "jsonb", - "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": { - "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": {} - }, - "public.questions": { - "name": "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 - }, - "title": { - "name": "title", - "type": "varchar(256)", - "primaryKey": false, - "notNull": true - }, - "sub_title": { - "name": "sub_title", - "type": "varchar(256)", - "primaryKey": false, - "notNull": false - }, - "vote_model": { - "name": "vote_model", - "type": "varchar(256)", - "primaryKey": false, - "notNull": true, - "default": "'COCM'" - }, - "show_score": { - "name": "show_score", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "user_can_create": { - "name": "user_can_create", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "fields": { - "name": "fields", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - }, - "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_cycle_id_cycles_id_fk": { - "name": "questions_cycle_id_cycles_id_fk", - "tableFrom": "questions", - "tableTo": "cycles", - "columnsFrom": [ - "cycle_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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": {} - }, - "public.options": { - "name": "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 - }, - "group_id": { - "name": "group_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "question_id": { - "name": "question_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "varchar(256)", - "primaryKey": false, - "notNull": true - }, - "sub_title": { - "name": "sub_title", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "show": { - "name": "show", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "vote_score": { - "name": "vote_score", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0.0'" - }, - "funding_request": { - "name": "funding_request", - "type": "numeric", - "primaryKey": false, - "notNull": false, - "default": "'0.0'" - }, - "data": { - "name": "data", - "type": "jsonb", - "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": { - "options_user_id_users_id_fk": { - "name": "options_user_id_users_id_fk", - "tableFrom": "options", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "options_registration_id_registrations_id_fk": { - "name": "options_registration_id_registrations_id_fk", - "tableFrom": "options", - "tableTo": "registrations", - "columnsFrom": [ - "registration_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "options_group_id_groups_id_fk": { - "name": "options_group_id_groups_id_fk", - "tableFrom": "options", - "tableTo": "groups", - "columnsFrom": [ - "group_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "options_question_id_questions_id_fk": { - "name": "options_question_id_questions_id_fk", - "tableFrom": "options", - "tableTo": "questions", - "columnsFrom": [ - "question_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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_options_id_fk": { - "name": "votes_option_id_options_id_fk", - "tableFrom": "votes", - "tableTo": "options", - "columnsFrom": [ - "option_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "votes_question_id_questions_id_fk": { - "name": "votes_question_id_questions_id_fk", - "tableFrom": "votes", - "tableTo": "questions", - "columnsFrom": [ - "question_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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 - }, - "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" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.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": {} - }, - "public.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": {} - }, - "public.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": {} - }, - "public.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": {} - }, - "public.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" - ] - } - } - }, - "public.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": {} - }, - "public.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": 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": { - "questions_to_group_categories_question_id_questions_id_fk": { - "name": "questions_to_group_categories_question_id_questions_id_fk", - "tableFrom": "questions_to_group_categories", - "tableTo": "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 f135dff4..7e3045fe 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -201,15 +201,8 @@ { "idx": 28, "version": "6", - "when": 1721839965156, - "tag": "0028_chilly_sentry", - "breakpoints": true - }, - { - "idx": 29, - "version": "6", - "when": 1722348223783, - "tag": "0029_outgoing_true_believers", + "when": 1722350190863, + "tag": "0028_keen_nick_fury", "breakpoints": true } ]