diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index a7d05777..17274f29 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -18,19 +18,25 @@ jobs: name: Review formatting timeout-minutes: 2 runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20] + steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - 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: 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..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@v2 + - 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 4759044f..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@v2 + - 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 579c3eee..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 - - uses: pnpm/action-setup@v2 + - 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 3eca7847..491027fc 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 @@ -38,20 +36,21 @@ 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 - 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 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 8e67a342..00000000 --- a/jest.config.mjs +++ /dev/null @@ -1,24 +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/'], - silent: true, // surpress console output for passing tests -}; 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/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/0028_keen_nick_fury.sql b/migrations/0028_keen_nick_fury.sql new file mode 100644 index 00000000..6d469850 --- /dev/null +++ b/migrations/0028_keen_nick_fury.sql @@ -0,0 +1,15 @@ +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 $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "status_idx" ON "cycles" ("status"); \ 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/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/0028_snapshot.json b/migrations/meta/0028_snapshot.json new file mode 100644 index 00000000..f7bb946c --- /dev/null +++ b/migrations/meta/0028_snapshot.json @@ -0,0 +1,1707 @@ +{ + "id": "38b232a4-f172-4406-8b6a-bcd48ee6c5b0", + "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": { + "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 f7d0afd6..7e3045fe 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -183,6 +183,27 @@ "when": 1718889966152, "tag": "0025_narrow_warhawk", "breakpoints": true + }, + { + "idx": 26, + "version": "6", + "when": 1719999199277, + "tag": "0026_redundant_galactus", + "breakpoints": true + }, + { + "idx": 27, + "version": "6", + "when": 1720035204142, + "tag": "0027_stormy_silverclaw", + "breakpoints": true + }, + { + "idx": 28, + "version": "6", + "when": 1722350190863, + "tag": "0028_keen_nick_fury", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 429e9cd8..21bd6b40 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": "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}\"" }, "keywords": [], "author": "", @@ -32,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", @@ -46,11 +44,12 @@ "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", + "tsx": "^4.16.2", "typescript": "^5.5.2" }, "dependencies": { @@ -61,6 +60,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..953cbc73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,13 +29,22 @@ 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 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 @@ -45,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 @@ -75,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 @@ -90,6 +96,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 @@ -295,6 +304,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'} @@ -307,6 +322,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'} @@ -319,6 +340,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'} @@ -331,6 +358,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'} @@ -343,6 +376,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'} @@ -355,6 +394,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'} @@ -367,6 +412,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'} @@ -379,6 +430,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'} @@ -391,6 +448,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'} @@ -403,6 +466,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'} @@ -415,6 +484,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'} @@ -427,6 +502,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'} @@ -439,6 +520,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'} @@ -451,6 +538,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'} @@ -463,6 +556,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'} @@ -475,6 +574,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'} @@ -487,6 +592,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'} @@ -499,6 +610,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'} @@ -511,6 +628,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'} @@ -523,6 +646,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'} @@ -535,6 +664,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'} @@ -547,6 +682,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'} @@ -559,6 +700,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} @@ -613,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'} @@ -826,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==} @@ -950,6 +1100,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'} @@ -964,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==} @@ -991,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==} @@ -1019,6 +1184,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'} @@ -1104,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'} @@ -1156,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'} @@ -1184,10 +1364,16 @@ 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} + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1199,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==} @@ -1258,6 +1447,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: @@ -1275,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: @@ -1294,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'} @@ -1440,6 +1640,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} @@ -1475,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==} @@ -1500,6 +1706,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'} @@ -1574,6 +1785,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 +1812,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 +1828,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==} @@ -1627,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'} @@ -1645,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'} @@ -1653,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==} @@ -1711,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==} @@ -1770,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'} @@ -1777,6 +2022,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==} @@ -1818,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==} @@ -1938,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==} @@ -1951,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'} @@ -1959,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'} @@ -2100,6 +2367,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==} @@ -2182,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==} @@ -2207,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'} @@ -2305,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==} @@ -2324,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'} @@ -2342,6 +2629,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'} @@ -2373,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==} @@ -2487,6 +2786,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 +2870,17 @@ 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==} + + 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 +2889,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 +2916,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,14 +2946,29 @@ 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'} + 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'} @@ -2666,12 +3014,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==} @@ -2696,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'} @@ -2743,6 +3101,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==} @@ -2753,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==} @@ -2803,6 +3168,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 +3222,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==} @@ -2902,6 +3273,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'} @@ -2918,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'} @@ -2941,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'} @@ -3006,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'} @@ -3026,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'} @@ -3033,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} @@ -3041,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'} @@ -3048,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'} @@ -3297,138 +3701,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 @@ -3504,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': @@ -3888,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': {} @@ -4053,6 +4526,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 @@ -4064,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 @@ -4092,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 @@ -4126,6 +4614,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 @@ -4261,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 @@ -4314,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 @@ -4322,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 @@ -4344,8 +4849,12 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + commander@9.5.0: {} + commondir@1.0.1: {} + concat-map@0.0.1: {} content-disposition@0.5.4: @@ -4354,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: {} @@ -4431,6 +4942,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 @@ -4441,6 +4954,8 @@ snapshots: optionalDependencies: supports-color: 5.5.0 + decamelize@1.2.0: {} + dedent@1.5.3: {} deep-eql@4.1.3: @@ -4451,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 @@ -4524,6 +5043,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: @@ -4608,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 @@ -4684,6 +5209,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: {} @@ -4782,6 +5333,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 +5399,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 +5415,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 @@ -4892,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 @@ -4914,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: @@ -5038,12 +5612,19 @@ 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 heap@0.2.7: {} + help-me@5.0.0: {} + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -5082,6 +5663,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -5188,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 @@ -5218,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 @@ -5545,6 +6145,8 @@ snapshots: - supports-color - ts-node + joycon@3.1.1: {} + js-sha256@0.10.1: {} js-sha3@0.8.0: {} @@ -5616,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: {} @@ -5640,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 @@ -5720,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: @@ -5747,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: {} @@ -5762,6 +6406,8 @@ snapshots: obuf@1.1.2: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -5799,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: @@ -5896,6 +6553,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 +6643,14 @@ 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: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -5951,6 +6661,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 +6684,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 +6715,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 @@ -6005,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 @@ -6050,12 +6783,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: {} @@ -6091,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 @@ -6138,6 +6877,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 @@ -6150,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 @@ -6211,6 +6963,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 +7019,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 @@ -6302,6 +7062,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 @@ -6312,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 @@ -6351,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: @@ -6421,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 @@ -6441,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 @@ -6449,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 @@ -6456,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 8d8645e0..9a3f77f1 100644 --- a/scripts/db/seed.ts +++ b/scripts/db/seed.ts @@ -1,6 +1,6 @@ +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() { if (process.argv.includes('--cleanup')) { @@ -15,7 +15,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 +27,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/utils/db/create-db-connection.ts b/src/db/create-db-connection.ts similarity index 84% rename from src/utils/db/create-db-connection.ts rename to src/db/create-db-connection.ts index c5e93bd4..4bc1ab31 100644 --- a/src/utils/db/create-db-connection.ts +++ b/src/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 * as schema from '../db/schema'; +import { logger } from '../utils/logger'; /** * creates a postgres database pool connection @@ -30,13 +31,13 @@ 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); }); return { pool, - db: drizzle(pool, { schema: db }), + db: drizzle(pool, { schema }), }; } @@ -68,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/cycles.ts b/src/db/cycles.ts deleted file mode 100644 index 4d0549d4..00000000 --- a/src/db/cycles.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { relations } from 'drizzle-orm'; -import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; -import { forumQuestions } from './forum-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 cyclesRelations = relations(cycles, ({ many, one }) => ({ - forumQuestions: many(forumQuestions), - event: one(events, { - fields: [cycles.eventId], - references: [events.id], - }), -})); - -export type Cycle = typeof cycles.$inferSelect; diff --git a/src/db/forum-questions.ts b/src/db/forum-questions.ts deleted file mode 100644 index f5d5f876..00000000 --- a/src/db/forum-questions.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 { questionsToGroupCategories } from './questions-to-group-categories'; - -export const forumQuestions = pgTable('forum_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 }), - 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 }) => ({ - cycle: one(cycles, { - fields: [forumQuestions.cycleId], - references: [cycles.id], - }), - questionOptions: many(questionOptions), - questionsToGroupCategories: many(questionsToGroupCategories), -})); - -export type ForumQuestion = typeof forumQuestions.$inferSelect; diff --git a/src/db/index.ts b/src/db/index.ts index eabeebc9..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 './forum-questions'; -export * from './registration-field-options'; -export * from './question-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 74% rename from src/db/comments.ts rename to src/db/schema/comments.ts index e378c0d0..550184a4 100644 --- a/src/db/comments.ts +++ b/src/db/schema/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/schema/cycles.ts b/src/db/schema/cycles.ts new file mode 100644 index 00000000..a911106f --- /dev/null +++ b/src/db/schema/cycles.ts @@ -0,0 +1,33 @@ +import { relations } from 'drizzle-orm'; +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(), + }, + (t) => ({ + statusIndex: index('status_idx').on(t.status), + }), +); + +export const cyclesRelations = relations(cycles, ({ many, one }) => ({ + questions: many(questions), + event: one(events, { + fields: [cycles.eventId], + references: [events.id], + }), +})); + +export type Cycle = typeof cycles.$inferSelect; diff --git a/src/db/events.ts b/src/db/schema/events.ts similarity index 71% rename from src/db/events.ts rename to src/db/schema/events.ts index 5d59ea29..3e385dc8 100644 --- a/src/db/events.ts +++ b/src/db/schema/events.ts @@ -1,7 +1,9 @@ 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'; +import { groupCategories } from './group-categories'; export const events = pgTable('events', { id: uuid('id').primaryKey().defaultRandom(), @@ -10,6 +12,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(), @@ -20,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; 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 94% rename from src/db/groups.ts rename to src/db/schema/groups.ts index c761a0d7..a9397db8 100644 --- a/src/db/groups.ts +++ b/src/db/schema/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/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/question-options.ts b/src/db/schema/options.ts similarity index 50% rename from src/db/question-options.ts rename to src/db/schema/options.ts index a9bd601d..36bab029 100644 --- a/src/db/question-options.ts +++ b/src/db/schema/options.ts @@ -1,42 +1,49 @@ -import { boolean, pgTable, timestamp, uuid, varchar, numeric } from 'drizzle-orm/pg-core'; -import { forumQuestions } from './forum-questions'; +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 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), + groupId: uuid('group_id').references(() => groups.id), questionId: uuid('question_id') - .references(() => forumQuestions.id) + .references(() => questions.id) .notNull(), - optionTitle: varchar('option_title', { length: 256 }).notNull(), - optionSubTitle: varchar('option_sub_title'), - accepted: boolean('accepted').default(false), + title: varchar('title', { length: 256 }).notNull(), + subTitle: varchar('sub_title'), + 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(), }); -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], }), + group: one(groups, { + fields: [options.groupId], + references: [groups.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/schema/questions-to-group-categories.ts similarity index 72% rename from src/db/questions-to-group-categories.ts rename to src/db/schema/questions-to-group-categories.ts index ef1af337..f63660a9 100644 --- a/src/db/questions-to-group-categories.ts +++ b/src/db/schema/questions-to-group-categories.ts @@ -1,14 +1,16 @@ 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), - groupCategoryId: uuid('group_category_id').references(() => groupCategories.id), // Must be nullable (for now) because affiliation does not have a group category id. + .references(() => questions.id), + groupCategoryId: uuid('group_category_id') + .notNull() + .references(() => groupCategories.id), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); @@ -16,9 +18,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/schema/questions.ts b/src/db/schema/questions.ts new file mode 100644 index 00000000..0817484b --- /dev/null +++ b/src/db/schema/questions.ts @@ -0,0 +1,31 @@ +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'; +import { questionsToGroupCategories } from './questions-to-group-categories'; + +export const questions = pgTable('questions', { + id: uuid('id').primaryKey().defaultRandom(), + cycleId: uuid('cycle_id') + .references(() => cycles.id) + .notNull(), + 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), + userCanCreate: boolean('user_can_create').default(false), + fields: jsonb('fields').notNull().default({}), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), +}); + +export const forumQuestionsRelations = relations(questions, ({ one, many }) => ({ + cycle: one(cycles, { + fields: [questions.cycleId], + references: [cycles.id], + }), + options: many(options), + questionsToGroupCategories: many(questionsToGroupCategories), +})); + +export type Question = typeof questions.$inferSelect; 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 92% rename from src/db/registrations.ts rename to src/db/schema/registrations.ts index db93020d..d69e9979 100644 --- a/src/db/registrations.ts +++ b/src/db/schema/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/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 93% rename from src/db/users.ts rename to src/db/schema/users.ts index 07e19ff2..9317c47c 100644 --- a/src/db/users.ts +++ b/src/db/schema/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/schema/votes.ts similarity index 71% rename from src/db/votes.ts rename to src/db/schema/votes.ts index ebedb537..fd77f846 100644 --- a/src/db/votes.ts +++ b/src/db/schema/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/db/seed.ts b/src/db/seed.ts new file mode 100644 index 00000000..24b1b96d --- /dev/null +++ b/src/db/seed.ts @@ -0,0 +1,549 @@ +import { + randBasketballTeam, + randBook, + randCity, + randDog, + randEmail, + 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 { dataSchema, 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 randCityFieldId = randUuid(); + const randAgeFieldId = randUuid(); + const events = await createEvent(dbPool, [ + { + name: randCity(), + fields: { + [randCityFieldId]: { + id: randCityFieldId, + name: 'city', + type: 'TEXT', + position: 0, + validation: { + required: true, + }, + }, + [randAgeFieldId]: { + id: randAgeFieldId, + name: 'age', + type: 'NUMBER', + position: 1, + 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, + }, + ]); + + 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, + 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 63728964..64989817 100644 --- a/src/handlers/alerts.ts +++ b/src/handlers/alerts.ts @@ -1,21 +1,22 @@ 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())), ), }); 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..c939b635 100644 --- a/src/handlers/auth.ts +++ b/src/handlers/auth.ts @@ -1,9 +1,10 @@ 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'; +import { logger } from '../utils/logger'; export function destroySessionHandler() { return function (req: Request, res: Response) { @@ -12,13 +13,13 @@ 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); 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 66005625..5c1f5a08 100644 --- a/src/handlers/comments.ts +++ b/src/handlers/comments.ts @@ -1,12 +1,13 @@ 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'; 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; @@ -15,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; @@ -47,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; @@ -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'] }); } }; @@ -76,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); @@ -85,7 +86,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' }] }); @@ -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); } }; @@ -107,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; @@ -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/cycles.ts b/src/handlers/cycles.ts index 911d9932..83d65090 100644 --- a/src/handlers/cycles.ts +++ b/src/handlers/cycles.ts @@ -1,21 +1,21 @@ 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: { - forumQuestions: { + questions: { with: { - questionOptions: { + options: { columns: { voteScore: false, }, - where: eq(db.questionOptions.accepted, 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 d2058b25..0f1e3883 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -1,9 +1,10 @@ -import { and, eq } from 'drizzle-orm'; +import { and, eq, sql } 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; @@ -12,15 +13,15 @@ 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: { - forumQuestions: { + questions: { with: { - questionOptions: { + options: { columns: { voteScore: false, }, - where: eq(db.questionOptions.accepted, true), + where: eq(schema.options.show, true), }, }, }, @@ -31,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; @@ -40,21 +41,41 @@ 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(); + 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 }); }; } -export function getEventHandler(dbPool: NodePgDatabase) { +export function getEventHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const { eventId } = req.params; @@ -63,14 +84,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) { @@ -85,14 +106,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 ?? ''; @@ -100,12 +121,15 @@ 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 }); } catch (e) { - console.log('error getting registration ' + e); + logger.error('error getting registration ' + e); return res.sendStatus(500); } }; 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); @@ -45,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; @@ -68,7 +63,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' }); } }; @@ -76,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; @@ -99,7 +88,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 ee065ce1..7c12304a 100644 --- a/src/handlers/options.ts +++ b/src/handlers/options.ts @@ -1,10 +1,20 @@ 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'; +import { insertOptionsSchema } from '../types'; +import { isUserIsPartOfGroup } from '../services/groups'; +import { + getUserOption, + saveOption, + updateOption, + canUserCreateOption, + 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; @@ -12,14 +22,14 @@ export function getOptionHandler(dbPool: NodePgDatabase) { return res.status(400).json({ error: 'Missing optionId' }); } - const { voteScore, ...rest } = getTableColumns(db.questionOptions); + const { voteScore, ...rest } = getTableColumns(schema.options); const rows = await dbPool .select({ ...rest, }) - .from(db.questionOptions) - .where(eq(db.questionOptions.id, optionId)); + .from(schema.options) + .where(eq(schema.options.id, optionId)); if (!rows.length) { return res.status(404).json({ error: 'Option not found' }); @@ -31,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 ?? ''; @@ -43,7 +51,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); } }; @@ -51,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; @@ -74,8 +76,113 @@ 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' }); } }; } + +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) { + logger.error('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) { + logger.error('error saving option ' + e); + return res.sendStatus(500); + } + }; +} diff --git a/src/handlers/forum-questions.ts b/src/handlers/questions.ts similarity index 67% rename from src/handlers/forum-questions.ts rename to src/handlers/questions.ts index c33376a0..67a7d050 100644 --- a/src/handlers/forum-questions.ts +++ b/src/handlers/questions.ts @@ -1,11 +1,12 @@ import type { Request, Response } from 'express'; -import * as db from '../db'; -import { getQuestionHearts } from '../services/forum-questions'; +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; @@ -21,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; @@ -44,7 +39,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' }); } }; @@ -52,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; @@ -77,7 +66,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 8b882f73..cfe04bdf 100644 --- a/src/handlers/registrations.ts +++ b/src/handlers/registrations.ts @@ -1,17 +1,18 @@ import type { Request, Response } from 'express'; -import * as db from '../db'; +import * as schema from '../db/schema'; import { insertRegistrationSchema } from '../types'; -import { validateRequiredRegistrationFields } from '../services/registration-fields'; import { saveRegistration, updateRegistration, - validateCreateRegistrationPermissions, - validateUpdateRegistrationPermissions, + getUserRegistration, + validateEventFields, } from '../services/registrations'; +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) { +export function getRegistrationDataHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const registrationId = req.params.id; const userId = req.session.userId; @@ -28,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 ?? [])]; @@ -40,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; @@ -50,38 +51,36 @@ 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 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 { 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); } }; } -export function updateRegistrationHandler(dbPool: NodePgDatabase) { +export function updateRegistrationHandler(dbPool: NodePgDatabase) { return async function (req: Request, res: Response) { const registrationId = req.params.id; @@ -97,38 +96,44 @@ 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 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) { - 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..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, @@ -13,8 +13,9 @@ 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) { +export function joinGroupsHandler(dbPool: NodePgDatabase) { return async (req: Request, res: Response) => { const userId = req.session.userId; const body = joinGroupsSchema.safeParse(req.body); @@ -28,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) { @@ -69,13 +70,13 @@ 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'] }); } }; } -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({ @@ -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'] }); @@ -106,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; @@ -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 1cc33fde..07a76c21 100644 --- a/src/handlers/users.ts +++ b/src/handlers/users.ts @@ -1,21 +1,20 @@ 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'; +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) { @@ -24,7 +23,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); } }; @@ -32,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; @@ -74,7 +71,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); } }; @@ -82,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; @@ -101,12 +96,12 @@ export function getUsersToGroupsHandler(dbPool: NodePgDatabase) { }, }, }, - where: eq(db.usersToGroups.userId, userId), + where: eq(schema.usersToGroups.userId, userId), }); 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' }); } }; @@ -114,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; @@ -134,18 +127,18 @@ 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 }); } catch (error: unknown) { - console.error(`[ERROR] ${JSON.stringify(error)}`); + logger.error(`error getting user attributes ${JSON.stringify(error)}`); return res.sendStatus(500); } }; } -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; @@ -164,18 +157,18 @@ 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(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; @@ -193,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 { @@ -205,7 +198,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 0f43a755..3cd7ab8c 100644 --- a/src/handlers/votes.ts +++ b/src/handlers/votes.ts @@ -1,16 +1,14 @@ import type { Request, Response } from 'express'; import { z } from 'zod'; -import * as db from '../db'; -import { saveVotes } from '../services/votes'; +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; @@ -29,15 +27,21 @@ 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}`); + 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..208a580b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +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() { @@ -25,7 +25,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/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/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..fe8e1486 100644 --- a/src/modules/plural-voting.spec.ts +++ b/src/modules/plural-voting.spec.ts @@ -1,4 +1,6 @@ +import assert from 'node:assert'; import { PluralVoting } from './plural-voting'; +import { describe, test } from 'node:test'; // Define instance outside the tests const groups: Record = { @@ -20,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'], @@ -40,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', () => { @@ -52,7 +54,7 @@ describe('commonGroup', () => { }; const result = pluralVoting.commonGroup('user0', 'user3', groupMemberships); - expect(result).toBe(false); + assert.equal(result, false); }); }); @@ -65,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', () => { @@ -75,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', () => { @@ -87,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', () => { @@ -97,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); }); }); @@ -107,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); }); }); @@ -122,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'], @@ -138,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'], @@ -150,7 +152,7 @@ describe('removeDuplicateGroups', () => { group0: ['user0'], }; const result = pluralVoting.removeDuplicateGroups(groups); - expect(result).toEqual({ + assert.deepEqual(result, { group0: ['user0'], }); }); @@ -166,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', () => { @@ -177,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', () => { @@ -188,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', () => { @@ -202,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', () => { @@ -216,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', () => { @@ -231,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', () => { @@ -242,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(); - console.log('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 84ebfe6e..941a8bf2 100644 --- a/src/modules/quadratic-voting.spec.ts +++ b/src/modules/quadratic-voting.spec.ts @@ -1,4 +1,6 @@ +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', () => { @@ -20,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('', () => { @@ -32,9 +34,7 @@ describe('quadraticVoting', () => { user3: 16, }; const [result, sum] = quadraticVoting(votes); - - console.log('Quadratic Votes:', result); - console.log('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/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/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 e30e815b..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'; @@ -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'; @@ -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 { @@ -30,9 +31,10 @@ export function apiRouter({ dbPool, cookiePassword, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; cookiePassword: string; }) { + const router = express.Router(); // setup router.use(express.json()); router.use(express.urlencoded({ extended: true })); @@ -48,13 +50,19 @@ 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 })); 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/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 cddb8cd2..55b37b99 100644 --- a/src/routers/options.ts +++ b/src/routers/options.ts @@ -1,16 +1,20 @@ import { default as express } from 'express'; -import type * as db from '../db'; +import type * as schema from '../db/schema'; import { getOptionUsersHandler, getOptionCommentsHandler, getOptionHandler, + saveOptionHandler, + updateOptionHandler, } from '../handlers/options'; import { isLoggedIn } from '../middleware/is-logged-in'; 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)); router.get('/:optionId/comments', isLoggedIn(dbPool), getOptionCommentsHandler(dbPool)); router.get('/:optionId/users', isLoggedIn(dbPool), getOptionUsersHandler(dbPool)); diff --git a/src/routers/forum-questions.ts b/src/routers/questions.ts similarity index 86% rename from src/routers/forum-questions.ts rename to src/routers/questions.ts index e515a588..9735fb46 100644 --- a/src/routers/forum-questions.ts +++ b/src/routers/questions.ts @@ -1,15 +1,15 @@ 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, getQuestionHeartsHandler, getResultStatisticsHandler, -} from '../handlers/forum-questions'; +} from '../handlers/questions'; 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 8ed45c5f..5958048e 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -1,22 +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, }) @@ -26,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, @@ -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 { @@ -43,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 414fa51e..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.QuestionOption | 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.questionOptions) + .update(schema.options) .set({ registrationId: registrationId!, userId: user?.id ?? '' }) - .where(eq(db.questionOptions.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 371368d9..ae1a4fe8 100644 --- a/src/services/comments.ts +++ b/src/services/comments.ts @@ -1,56 +1,51 @@ 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, - questionOptionId: data.questionOptionId, + optionId: data.optionId, value: data.value, }) .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'); } } /** * 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) { @@ -58,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; }, @@ -78,15 +73,15 @@ 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.questionOptionId, 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 { 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: { @@ -103,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, ) { @@ -120,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; @@ -149,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<{ @@ -205,15 +195,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 +215,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 ( @@ -254,7 +244,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/cycles.spec.ts b/src/services/cycles.spec.ts index e5ad1bc8..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.QuestionOption | undefined; - let forumQuestion: db.ForumQuestion | 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 26e0f93d..5d3e6091 100644 --- a/src/services/cycles.ts +++ b/src/services/cycles.ts @@ -1,14 +1,14 @@ 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: { - 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(schema.options.show, 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, + show: option.show, + title: option.title, + subTitle: option.subTitle, questionId: option.questionId, voteScore: question.showScore ? option.voteScore : undefined, registrationId: option.registrationId, @@ -73,22 +73,22 @@ 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, ) { const response = await dbPool.query.cycles.findMany({ with: { - forumQuestions: { + questions: { with: { - questionOptions: { + options: { columns: { voteScore: false, }, 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,13 +100,11 @@ export async function getCycleVotes( }, }, }, - where: eq(db.cycles.id, cycleId), + where: eq(schema.cycles.id, cycleId), }); 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.spec.ts b/src/services/funding-mechanism.spec.ts new file mode 100644 index 00000000..098e5670 --- /dev/null +++ b/src/services/funding-mechanism.spec.ts @@ -0,0 +1,40 @@ +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 { calculateFunding } from './funding-mechanism'; + +describe('service: funding-mechanism', () => { + let dbPool: NodePgDatabase; + let question: schema.Question; + let deleteTestDatabase: () => Promise; + + before(async () => { + const envVariables = environmentVariables.parse(process.env); + 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'); + 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); + assert(response.allocatedFunding); + assert.equal(response.remainingFunding, 100000); + assert.equal(response.error, null); + }); + + after(async () => { + await deleteTestDatabase(); + }); +}); diff --git a/src/services/funding-mechanism.ts b/src/services/funding-mechanism.ts index 85e52d0a..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,27 +12,39 @@ 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<{ 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.questionOptions.id, - voteScore: db.questionOptions.voteScore, - fundingRequest: db.questionOptions.fundingRequest, + id: schema.options.id, + voteScore: schema.options.voteScore, + fundingRequest: schema.options.fundingRequest, }) - .from(db.questionOptions) - .where(eq(db.questionOptions.questionId, forumQuestionId)); + .from(schema.options) + .where(eq(schema.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/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 92667b9d..f3f15523 100644 --- a/src/services/groups.spec.ts +++ b/src/services/groups.spec.ts @@ -1,18 +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[] = [ @@ -45,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 = { @@ -91,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 () { @@ -116,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 () { @@ -134,24 +117,89 @@ 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(schema.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, + }); + + assert.equal(result, false); + }); + test('when the user is in the group', async function () { + const rows = await dbPool + .insert(schema.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(schema.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, + }); + + 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 224d7610..e400beea 100644 --- a/src/services/groups.ts +++ b/src/services/groups.ts @@ -1,18 +1,18 @@ -import * as db from '../db'; +import { and, eq } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { z } from 'zod'; +import * as schema from '../db/schema'; import { insertGroupsSchema } from '../types/groups'; -import { wordlist } from '../utils/db/mnemonics'; -import { eq } from 'drizzle-orm'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +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, }, @@ -98,3 +95,28 @@ 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(schema.usersToGroups.userId, userId), + eq(schema.usersToGroups.groupId, groupId), + ), + }); + + if (!userGroup) { + return false; + } + } + + return true; +} 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 new file mode 100644 index 00000000..582d45b4 --- /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 schema from '../db/schema'; +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(schema.options.userId, userId), eq(schema.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: schema.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(schema.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: schema.Option, + body: z.infer, +) { + const rows = await dbPool + .update(schema.options) + .set({ + groupId: body.groupId, + questionId: body.questionId, + data: body.data, + title: body.title, + subTitle: body.subTitle, + updatedAt: new Date(), + }) + .where(and(eq(schema.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(schema.questions) + .where(eq(schema.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(schema.questions) + .where(eq(schema.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 85% rename from src/services/forum-questions.spec.ts rename to src/services/questions.spec.ts index c1f3f737..3d65ade5 100644 --- a/src/services/forum-questions.spec.ts +++ b/src/services/questions.spec.ts @@ -1,4 +1,6 @@ -import { availableHearts } from './forum-questions'; +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/forum-questions.ts b/src/services/questions.ts similarity index 50% rename from src/services/forum-questions.ts rename to src/services/questions.ts index e234b7f4..025d87f8 100644 --- a/src/services/forum-questions.ts +++ b/src/services/questions.ts @@ -1,7 +1,16 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import * as db from '../db'; -import { sql } from 'drizzle-orm'; +import * as schema from '../db/schema'; +import { eq, sql } from 'drizzle-orm'; +import { z } from 'zod'; +import { insertOptionsSchema } from '../types/options'; +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, @@ -9,21 +18,12 @@ 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; } 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; } @@ -31,7 +31,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; } @@ -39,7 +39,7 @@ export function availableHearts( } export async function getQuestionHearts( - dbPool: NodePgDatabase, + dbPool: NodePgDatabase, data: { forumQuestionId: string; }, @@ -50,7 +50,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}' `), ); @@ -71,3 +71,38 @@ export async function getQuestionHearts( return 0; } } + +export async function validateQuestionFields({ + option, + dbPool, +}: { + dbPool: NodePgDatabase; + option: z.infer; +}) { + const rows = await dbPool + .select() + .from(schema.questions) + .where(eq(schema.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 deleted file mode 100644 index febe639c..00000000 --- a/src/services/registrations.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -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 { cleanup, seed } from '../utils/db/seed'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Client } from 'pg'; - -describe('service: registrations', () => { - let dbPool: NodePgDatabase; - let dbConnection: Client; - let testData: 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, 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', - }, - ], - }; - }); - 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'); - }); - // 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'); - }); - // check timestamps - expect(response.createdAt).toEqual(expect.any(Date)); - expect(response.updatedAt).toEqual(expect.any(Date)); - - // Check if the value was updated - expect(response.registrationData?.[0]?.value).toEqual('updated'); - }); - afterAll(async () => { - // Delete registration data - await cleanup(dbPool); - await dbConnection.end(); - }); -}); diff --git a/src/services/registrations.ts b/src/services/registrations.ts index f0f76acb..4de8a04b 100644 --- a/src/services/registrations.ts +++ b/src/services/registrations.ts @@ -1,78 +1,82 @@ import { and, eq } from 'drizzle-orm'; -import { z } from 'zod'; -import { insertRegistrationSchema } from '../types'; -import * as db from '../db'; -import { upsertRegistrationData } from './registration-data'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { z } from 'zod'; +import * as schema from '../db/schema'; +import { fieldsSchema, insertRegistrationSchema } from '../types'; +import { enforceRules } from './validation'; -export async function validateCreateRegistrationPermissions({ +export async function getUserRegistration({ dbPool, + registrationId, userId, - groupId, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; userId: string; - groupId?: string | null; -}) { - if (groupId) { - const userGroup = dbPool.query.usersToGroups.findFirst({ - where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, groupId)), - }); - - if (!userGroup) { - return false; - } + registrationId: string; +}): Promise { + const existingRegistration = await dbPool.query.registrations.findFirst({ + where: and( + eq(schema.registrations.userId, userId), + eq(schema.registrations.id, registrationId), + ), + }); + + if (!existingRegistration) { + return null; } - return true; + return existingRegistration; } -export async function validateUpdateRegistrationPermissions({ +export async function validateEventFields({ + registration, dbPool, - registrationId, - userId, - groupId, }: { - dbPool: NodePgDatabase; - userId: string; - registrationId: string; - groupId?: string | null; + dbPool: NodePgDatabase; + registration: z.infer; }) { - const existingRegistration = await dbPool.query.registrations.findFirst({ - where: and(eq(db.registrations.userId, userId), eq(db.registrations.id, registrationId)), - }); + const rows = await dbPool + .select() + .from(schema.events) + .where(eq(schema.events.id, registration.eventId)); - if (!existingRegistration) { - return false; + if (!rows.length) { + return []; } - if (existingRegistration.userId !== userId) { - return false; + const event = rows[0]; + + if (!event) { + return []; } - if (groupId) { - const userGroup = dbPool.query.usersToGroups.findFirst({ - where: and(eq(db.usersToGroups.userId, userId), eq(db.usersToGroups.groupId, groupId)), - }); + // get fields for the event + const eventFields = fieldsSchema.safeParse(event.fields); - if (!userGroup) { - return false; - } + if (!eventFields.success) { + return []; } - return true; + return enforceRules({ + data: registration.data, + fields: eventFields.data, + }); } export async function saveRegistration( - dbPool: NodePgDatabase, - data: z.infer, + dbPool: NodePgDatabase, + registration: z.infer, ) { const event = await dbPool.query.events.findFirst({ - where: eq(db.events.id, data.eventId), + where: eq(schema.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 +84,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; @@ -101,57 +94,37 @@ export async function saveRegistration( export async function updateRegistration({ data, dbPool, - registrationId, - userId, + registration, }: { - dbPool: NodePgDatabase; + dbPool: NodePgDatabase; data: z.infer; - registrationId: string; - userId: string; + registration: schema.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'); } - 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; } 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, eventId: body.eventId, + data: body.data, status: body.status, }) .returning(); @@ -159,18 +132,54 @@ 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]; } + +export async function validateRegistrationData({ + registration, + dbPool, +}: { + dbPool: NodePgDatabase; + registration: z.infer; +}) { + const rows = await dbPool + .select() + .from(schema.events) + .where(eq(schema.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, + }); +} diff --git a/src/services/statistics.spec.ts b/src/services/statistics.spec.ts index a4c3c2e6..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.QuestionOption | undefined; - let forumQuestion: db.ForumQuestion | 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?.optionTitle).toBeDefined(); - expect(optionStat?.optionSubTitle).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 86bc1486..f3854ee2 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 * as schema from '../db/schema'; import { sql } from 'drizzle-orm'; +import { logger } from '../utils/logger'; type ResultData = { numProposals: number; @@ -10,8 +11,8 @@ type ResultData = { optionStats: Record< string, { - optionTitle: string; - optionSubTitle: string; + title: string; + subTitle: string; pluralityScore: string; distinctUsers: number; allocatedHearts: number; @@ -24,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 @@ -46,9 +43,9 @@ 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 + AND show = TRUE `), ), @@ -100,8 +97,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,10 +115,10 @@ 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 + AND show = TRUE -- makes sure to only expose data of accepted options ), allocated_hearts AS ( @@ -189,8 +186,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 +218,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 +233,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 +244,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, @@ -268,7 +265,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.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 36f8f3e1..4a56bf23 100644 --- a/src/services/users-to-groups.ts +++ b/src/services/users-to-groups.ts @@ -1,32 +1,33 @@ 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) { - console.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) { - 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'); } return await dbPool - .insert(db.usersToGroups) + .insert(schema.usersToGroups) .values({ userId, groupId, groupCategoryId: group.groupCategoryId }) .returning(); } @@ -37,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) { - console.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) { @@ -60,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, ) { @@ -75,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) { @@ -83,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 @@ -93,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), ), }); @@ -103,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 f85fe930..ba64966e 100644 --- a/src/services/users.spec.ts +++ b/src/services/users.spec.ts @@ -1,26 +1,157 @@ +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 { 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(''); - } - }); +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 userData: { + email: string | null; + username: string | null; + firstName: string | null; + lastName: string | null; + telegram: string | null; + }; + let user: schema.User; + let secondUser: schema.User; + let deleteTestDatabase: () => Promise; + + before(async () => { + const envVariables = environmentVariables.parse(process.env); + const { dbClient, teardown } = await createTestDatabase(envVariables); + dbPool = dbClient.db; + deleteTestDatabase = teardown; + // 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)) { + assert.notStrictEqual(transformedUser[key], ''); + } + }); + + 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); + assert(response !== null); + assert(response?.length > 0); + assert(response?.[0] !== null); + }); + + 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); + assert(response !== null); + assert(response?.length > 0); + assert(response?.[0] !== null); + }); + + 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); + assert(response === null); + }); + + 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); + 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 () => { + 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); + assert(response.errors); + assert(response.errors?.length > 0); + assert(response.errors![0] !== null); + }); + + 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); + 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); + }); + + after(async () => { + await deleteTestDatabase(); }); }); diff --git a/src/services/users.ts b/src/services/users.ts index 41058eef..be1148f0 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -1,29 +1,26 @@ -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'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +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. */ -async function validateUserData( - dbPool: NodePgDatabase, +export async function validateUserData( + 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)), ), ); @@ -47,18 +44,15 @@ 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. */ -async function upsertUserData( - dbPool: NodePgDatabase, +export async function upsertUserData( + dbPool: NodePgDatabase, userId: string, userData: UserData, ) { try { const user = await dbPool - .update(db.users) + .update(schema.users) .set({ email: userData.email, username: userData.username, @@ -67,22 +61,20 @@ async function upsertUserData( telegram: userData.telegram, updatedAt: new Date(), }) - .where(eq(db.users.id, userId)) + .where(eq(schema.users.id, userId)) .returning(); return user; } catch (error) { - console.error('Failed to update user data:', error); + logger.error('Failed to update user data:', error); } } /** * 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 new file mode 100644 index 00000000..5d44f0c8 --- /dev/null +++ b/src/services/validation.spec.ts @@ -0,0 +1,394 @@ +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 () { + test('should return an error if a required field is missing', function () { + const fields: z.infer = { + name: { + id: 'name', + name: 'Name', + position: 1, + type: 'TEXT', + validation: { + required: true, + }, + }, + }; + const data: z.infer = {}; + + const result = enforceRules({ data, fields }); + + assert.equal(result.length, 1); + assert.deepEqual(result, ['Name is required']); + }); + + test('should not return an error if a required field is present', function () { + const fields: z.infer = { + name: { + 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 }); + + assert.equal(result.length, 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 = { + name: { + 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 }); + + 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 () { + const fields: z.infer = { + name: { + 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 }); + + assert.equal(result.length, 0); + }); + }); + describe('rule: maxLength', function () { + test('should return an error if the string is too long', function () { + const fields: z.infer = { + name: { + 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 }); + + 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 () { + const fields: z.infer = { + name: { + 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 }); + + assert.equal(result.length, 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 = { + age: { + 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 }); + + 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 () { + const fields: z.infer = { + age: { + 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 }); + + assert.equal(result.length, 0); + }); + }); + describe('rule: maxLength', function () { + test('should return an error if the number is too large', function () { + const fields: z.infer = { + age: { + 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 }); + + 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 = { + age: { + 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 }); + + assert.equal(result.length, 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 = { + colors: { + 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 }); + + 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 = { + colors: { + 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 }); + + assert.equal(result.length, 0); + }); + }); + describe('rule: maxLength', function () { + test('should return an error if the array is too large', function () { + const fields: z.infer = { + colors: { + 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 }); + + 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 = { + colors: { + 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 }); + + assert.equal(result.length, 0); + }); + }); + }); +}); diff --git a/src/services/validation.ts b/src/services/validation.ts new file mode 100644 index 00000000..415204c1 --- /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 Object.values(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/services/votes.spec.ts b/src/services/votes.spec.ts index 4a71b852..9ffc780c 100644 --- a/src/services/votes.spec.ts +++ b/src/services/votes.spec.ts @@ -1,60 +1,46 @@ -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 { z } from 'zod'; +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 { - saveVote, - queryVoteData, - queryGroupCategories, - numOfVotesDictionary, - groupsDictionary, calculatePluralScore, calculateQuadraticScore, + groupsDictionary, + numOfVotesDictionary, + queryGroupCategories, + queryVoteData, + saveVote, + updateOptionScore, updateVoteScoreInDatabase, - updateVoteScore, + updateVoteScorePlural, + updateVoteScoreQuadratic, userCanVote, + 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 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 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 dbPool: NodePgDatabase; + let deleteTestDatabase: () => Promise; + let testData: { optionId: string; numOfVotes: number }; + 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 @@ -64,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]; @@ -72,89 +57,188 @@ describe('service: votes', () => { testData = { numOfVotes: 1, optionId: questionOption?.id ?? '', - questionId: forumQuestion?.id ?? '', - userId: user?.id ?? '', }; }); - test('should save vote', async () => { - await dbPool.update(db.cycles).set({ status: 'OPEN' }).where(eq(db.cycles.id, cycle!.id)); - // accept user registration - await dbPool.insert(db.registrations).values({ + test('validation should return false if no option id is specified', async () => { + const response = await validateVote(dbPool, { numOfVotes: 1, optionId: '' }, user!.id ?? ''); + assert.equal(response.isValid, false); + assert(response.error); + }); + + 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 ?? '', + ); + assert.equal(response.isValid, false); + assert(response.error); + }); + + test('validation should return false if the cycle is not open', async () => { + 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 ?? '', + ); + assert.equal(response.isValid, false); + assert(response.error); + }); + + test('validation should return false if a user is not approved', async () => { + 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 ?? '', + ); + + assert.equal(response.isValid, false); + assert(response.error); + }); + + test('validation should return true all validation checks pass', async () => { + await dbPool.insert(schema.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 - 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)); + const response = await validateVote( + dbPool, + { numOfVotes: 1, optionId: questionOption?.id ?? '' }, + user!.id ?? '', + ); + + assert.equal(response.isValid, true); + assert.equal(response.error, null); }); - 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('userCanVote returns false if user does not have an approved registration', async () => { + const response = await userCanVote(dbPool, secondUser!.id ?? '', questionOption?.id ?? ''); + assert.equal(response, false); + }); - // expect response to be undefined - expect(response).toBeUndefined(); + test('userCanVote returns true if user has an approved registration', async () => { + await dbPool.insert(schema.registrations).values({ + status: 'APPROVED', + userId: secondUser!.id ?? '', + eventId: cycle!.eventId ?? '', + }); + const response = await userCanVote(dbPool, secondUser!.id ?? '', questionOption?.id ?? ''); + assert.equal(response, true); + }); - // expect error message - expect(errors).toBeDefined(); + test('userCanVote returns false if no option id gets provided', async () => { + const response = await userCanVote(dbPool, secondUser!.id ?? '', ''); + assert.equal(response, false); }); - 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('should save vote', async () => { + const { data: response } = await saveVote( + dbPool, + testData, + user?.id ?? '', + forumQuestion?.id ?? '', + ); + 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 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); + 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 ?? '', + ); + assert.equal(response.data, null); + assert(response.error); + }); - // 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); + assert.equal(response.data, null); + assert(response.errors); + assert(response.errors[0]); }); - 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)); + 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', + ]; + const response = await updateOptionScore(dbPool, mockData, questionIds); + 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 () => { + const mockData = [ + { optionId: 'option1', numOfVotes: 10 }, + { optionId: 'option2', numOfVotes: 5 }, + { optionId: 'option3', numOfVotes: 2 }, + ]; + const questionIds = ['', '']; + + const response = await updateOptionScore(dbPool, mockData, questionIds); + 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 ?? '', }); - // 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); + 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', () => { @@ -167,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', () => { @@ -181,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, @@ -199,28 +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.length).toBe(1); - expect(Array.isArray(groupCategoriesIdArray)).toBe(true); - groupCategoriesIdArray.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.length).toBe(0); - expect(Array.isArray(groupCategoriesIdArray)).toBe(true); - expect(groupCategoriesIdArray).toEqual([]); + assert(groupCategoriesIdArray); + assert.equal(groupCategoriesIdArray.data, null); }); test('only return groups for users who voted for the option', async () => { @@ -228,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 () => { @@ -243,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', () => { @@ -283,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', () => { @@ -303,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', () => { @@ -316,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 () => { @@ -324,24 +400,35 @@ describe('service: votes', () => { const score = 100; 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 ?? ''), + // query updated db in schema + const updatedDbScore = await dbPool.query.options.findFirst({ + where: eq(schema.options.id, questionOption?.id ?? ''), }); - expect(updatedDbScore?.voteScore).toBe('100'); + assert(updatedDbScore); + assert(updatedDbScore.voteScore); + assert.equal(updatedDbScore.voteScore, '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 updateVoteScore(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)); + 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 + 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 2700c117..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'; @@ -9,42 +8,140 @@ 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 each vote provided in the `data` array for the specified `userId`. + * If all votes are valid, it saves each vote to the database. */ -export async function saveVotes( - dbPool: NodePgDatabase, +export async function validateAndSaveVotes( + dbPool: NodePgDatabase, data: { optionId: string; numOfVotes: number }[], userId: string, -): Promise<{ data: db.Vote[]; errors: string[] }> { - const out: db.Vote[] = []; +): Promise<{ data: schema.Vote[] | null; questionIds: string[]; errors: string[] }> { + const voteData: schema.Vote[] = []; + const questionIds: string[] = []; const errors: string[] = []; for (const vote of data) { - const { data, error } = await validateAndSaveVote(dbPool, vote, userId); - if (data) { - out.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(schema.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 uniqueOptionIds = new Set(out.map((vote) => vote.optionId)); + return { data: voteData.length > 0 ? voteData : null, questionIds, errors }; +} + +/** + * Updates option scores based on the vote model. + */ +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[] = []; - // Update the vote count for each option - for (const optionId of uniqueOptionIds) { - await updateVoteScore(dbPool, optionId); + // 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 }; } - return { data: out, errors }; + if (!firstQuestionId) { + errors.push('No question Id found'); + return { data: null, errors }; + } + + // Query group data, grouping dimensions, and calculate the score + const queryQuestion = await dbPool + .select({ + questionId: schema.questions.id, + voteModel: schema.questions.voteModel, + }) + .from(schema.questions) + .where(eq(schema.questions.id, firstQuestionId)); + + if (queryQuestion.length === 0) { + errors.push('No question found for the provided questionId'); + return { data: null, errors }; + } + + const voteModel = queryQuestion[0]?.voteModel; + + interface VoteModelUpdateFunction { + ({ + dbPool, + optionId, + questionId, + }: { + dbPool: NodePgDatabase; + optionId: string; + questionId: string; + }): Promise; + } + + const voteModelUpdateFunctions: Record = { + COCM: ({ dbPool, optionId, questionId }) => updateVoteScorePlural(dbPool, optionId, questionId), + QV: ({ dbPool, optionId }) => updateVoteScoreQuadratic(dbPool, optionId), + }; + + const updateFunction = + voteModelUpdateFunctions[voteModel as keyof typeof voteModelUpdateFunctions]; + + if (!updateFunction) { + errors.push('Unsupported vote model: ' + voteModel); + return { data: null, 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 }; } /** 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" @@ -78,36 +175,40 @@ 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, + dbPool: NodePgDatabase, questionId: string, -): Promise { +): 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)); - - // Need to due this adjustment because currently groupCategoryId is nullable in the datatable definition. - const groupCategoryIds: string[] = groupCategories.map((category) => category.groupCategoryId!); + .from(schema.questionsToGroupCategories) + .where(eq(schema.questionsToGroupCategories.questionId, questionId)); - 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 }; } /** * 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, ) { @@ -158,178 +259,172 @@ 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, ) { - // Update vote score in the database await dbPool - .update(db.questionOptions) + .update(schema.options) .set({ voteScore: score.toString(), updatedAt: new Date(), }) - .where(eq(db.questionOptions.id, optionId)); + .where(eq(schema.options.id, optionId)); } /** - * 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 * 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 updateVoteScore( - dbPool: NodePgDatabase, +export async function updateVoteScorePlural( + dbPool: NodePgDatabase, optionId: string, + questionId: 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); + const groupCategories = await queryGroupCategories(dbPool, questionId); + const groupArray = await groupsDictionary(dbPool, votesDictionary, groupCategories.data!); + const score = await calculatePluralScore(groupArray, votesDictionary); - // Query question Id - const queryQuestionId = await dbPool - .select({ - questionId: db.questionOptions.questionId, - }) - .from(db.questionOptions) - .where(eq(db.questionOptions.id, optionId)); - - // Query group categories - const groupCategories = await queryGroupCategories(dbPool, queryQuestionId[0]!.questionId); - - // Query group data - const groupArray = await groupsDictionary(dbPool, votesDictionary, groupCategories ?? []); + await updateVoteScoreInDatabase(dbPool, optionId, score); - // Perform plural voting calculation - const score = await calculatePluralScore(groupArray, votesDictionary); + return score; +} - // Perform quadratic score calculation - // const score = await calculateQuadraticScore(votesDictionary); +/** + * 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; } /** - * Validates and saves a vote for a user in the database. - * - * 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. + * 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. * - * @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. + * If all validations pass, the vote is considered valid. Otherwise, an appropriate error message is returned. */ -async function validateAndSaveVote( - dbPool: NodePgDatabase, +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' }; } - const queryQuestionOption = await dbPool.query.questionOptions.findFirst({ - where: eq(db.questionOptions.id, vote.optionId), + // check if the option exists + const queryQuestionOption = await dbPool.query.options.findFirst({ + where: eq(schema.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(schema.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' }; + return { isValid: false, 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 { 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. */ export async function saveVote( - dbPool: NodePgDatabase, - vote: z.infer, -) { - // check if cycle is open - const queryQuestion = await dbPool.query.forumQuestions.findFirst({ - where: eq(db.forumQuestions.id, vote?.questionId ?? ''), - with: { - cycle: true, - }, - }); + dbPool: NodePgDatabase, + vote: { optionId: string; numOfVotes: number }, + userId: string, + questionId: string, +): Promise<{ data: schema.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) + .insert(schema.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 }; } /** * 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, ) { @@ -339,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/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..b2c1eb9e --- /dev/null +++ b/src/types/options.ts @@ -0,0 +1,7 @@ +import { createInsertSchema } from 'drizzle-zod'; +import { dataSchema } from './validation'; +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 635ed858..8e29d865 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 { registrations } from '../db/schema/registrations'; +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/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/validation.ts b/src/types/validation.ts new file mode 100644 index 00000000..5a0b47fa --- /dev/null +++ b/src/types/validation.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +/** + * Fields schema + */ + +const fieldType = z.enum(['TEXT', 'TEXTAREA', 'SELECT', 'CHECKBOX', 'MULTI_SELECT', 'NUMBER']); + +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 + */ + +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().uuid(), dataForOneFieldSchema); 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 e642c232..00000000 --- a/src/utils/db/seed-data-generators.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { randCompanyName, randCountry, randUser } from '@ngneat/falso'; -import { - Cycle, - Event, - ForumQuestion, - RegistrationField, - RegistrationFieldOption, - QuestionOption, - GroupCategory, - Group, - User, - UsersToGroups, - QuestionsToGroupCategories, -} from '../../db'; - -// 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[] = []; - for (let i = 0; i < numEvents; i++) { - events.push({ name: randCountry() }); - } - 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[], -): ForumQuestionData[] { - return questionTitles.map((questionTitle) => ({ - cycleId, - questionTitle, - })); -} - -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, - optionTitle: optionTitles[i]!, - accepted: 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 39c43b62..00000000 --- a/src/utils/db/seed.ts +++ /dev/null @@ -1,438 +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']), - ); - 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.questionOptions); - await dbPool.delete(db.registrationData); - await dbPool.delete(db.registrationFieldOptions); - await dbPool.delete(db.registrationFields); - await dbPool.delete(db.registrations); - await dbPool.delete(db.usersToGroups); - await dbPool.delete(db.users); - await dbPool.delete(db.groups); - await dbPool.delete(db.questionsToGroupCategories); - await dbPool.delete(db.groupCategories); - await dbPool.delete(db.forumQuestions); - await dbPool.delete(db.cycles); - await dbPool.delete(db.events); -} - -async function createEvent(dbPool: NodePgDatabase, eventData: EventData[]) { - const events = []; - for (const eventName of eventData) { - const result = await dbPool - .insert(db.events) - .values({ - name: eventName.name, - }) - .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.forumQuestions) - .values({ - cycleId: questionData.cycleId, - questionTitle: questionData.questionTitle, - }) - .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.questionOptions) - .values({ - questionId: questionOption.questionId, - optionTitle: questionOption.optionTitle, - accepted: questionOption.accepted, - }) - .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 new file mode 100644 index 00000000..2b14a06f --- /dev/null +++ b/src/utils/logger/index.ts @@ -0,0 +1,15 @@ +import pino from 'pino'; +import pretty from 'pino-pretty'; + +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', + }, + 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