diff --git a/website/drizzle/0000_spooky_umar.sql b/website/drizzle/0000_crazy_bloodstrike.sql similarity index 86% rename from website/drizzle/0000_spooky_umar.sql rename to website/drizzle/0000_crazy_bloodstrike.sql index dad4224..f7e2008 100644 --- a/website/drizzle/0000_spooky_umar.sql +++ b/website/drizzle/0000_crazy_bloodstrike.sql @@ -1,3 +1,9 @@ +DO $$ BEGIN + CREATE TYPE "public"."notification_type" AS ENUM('HOPIUM', 'SYSTEM', 'TRANSFER', 'RUG_PULL'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint DO $$ BEGIN CREATE TYPE "public"."prediction_market_status" AS ENUM('ACTIVE', 'RESOLVED', 'CANCELLED'); EXCEPTION @@ -5,7 +11,7 @@ EXCEPTION END $$; --> statement-breakpoint DO $$ BEGIN - CREATE TYPE "public"."transaction_type" AS ENUM('BUY', 'SELL'); + CREATE TYPE "public"."transaction_type" AS ENUM('BUY', 'SELL', 'TRANSFER_IN', 'TRANSFER_OUT'); EXCEPTION WHEN duplicate_object THEN null; END $$; @@ -47,7 +53,7 @@ CREATE TABLE IF NOT EXISTS "coin" ( "current_price" numeric(20, 8) NOT NULL, "market_cap" numeric(30, 2) NOT NULL, "volume_24h" numeric(30, 2) DEFAULT '0.00', - "change_24h" numeric(10, 4) DEFAULT '0.0000', + "change_24h" numeric(30, 4) DEFAULT '0.0000', "pool_coin_amount" numeric(30, 8) DEFAULT '0.00000000' NOT NULL, "pool_base_currency_amount" numeric(30, 8) DEFAULT '0.00000000' NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, @@ -74,6 +80,16 @@ CREATE TABLE IF NOT EXISTS "comment_like" ( CONSTRAINT "comment_like_user_id_comment_id_pk" PRIMARY KEY("user_id","comment_id") ); --> statement-breakpoint +CREATE TABLE IF NOT EXISTS "notification" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "type" "notification_type" NOT NULL, + "title" varchar(200) NOT NULL, + "message" text NOT NULL, + "is_read" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint CREATE TABLE IF NOT EXISTS "prediction_bet" ( "id" serial PRIMARY KEY NOT NULL, "user_id" integer, @@ -149,7 +165,9 @@ CREATE TABLE IF NOT EXISTS "transaction" ( "quantity" numeric(30, 8) NOT NULL, "price_per_coin" numeric(20, 8) NOT NULL, "total_base_currency_amount" numeric(30, 8) NOT NULL, - "timestamp" timestamp with time zone DEFAULT now() NOT NULL + "timestamp" timestamp with time zone DEFAULT now() NOT NULL, + "recipient_user_id" integer, + "sender_user_id" integer ); --> statement-breakpoint CREATE TABLE IF NOT EXISTS "user" ( @@ -234,6 +252,12 @@ EXCEPTION WHEN duplicate_object THEN null; END $$; --> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "notification" ADD CONSTRAINT "notification_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint DO $$ BEGIN ALTER TABLE "prediction_bet" ADD CONSTRAINT "prediction_bet_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; EXCEPTION @@ -294,6 +318,18 @@ EXCEPTION WHEN duplicate_object THEN null; END $$; --> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "transaction" ADD CONSTRAINT "transaction_recipient_user_id_user_id_fk" FOREIGN KEY ("recipient_user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "transaction" ADD CONSTRAINT "transaction_sender_user_id_user_id_fk" FOREIGN KEY ("sender_user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint DO $$ BEGIN ALTER TABLE "user_portfolio" ADD CONSTRAINT "user_portfolio_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; EXCEPTION @@ -311,6 +347,10 @@ CREATE INDEX IF NOT EXISTS "account_deletion_request_scheduled_deletion_idx" ON CREATE INDEX IF NOT EXISTS "account_deletion_request_open_idx" ON "account_deletion_request" USING btree ("user_id") WHERE is_processed = false;--> statement-breakpoint CREATE INDEX IF NOT EXISTS "comment_user_id_idx" ON "comment" USING btree ("user_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "comment_coin_id_idx" ON "comment" USING btree ("coin_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "notification_user_id_idx" ON "notification" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "notification_type_idx" ON "notification" USING btree ("type");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "notification_is_read_idx" ON "notification" USING btree ("is_read");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "notification_created_at_idx" ON "notification" USING btree ("created_at");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "prediction_bet_user_id_idx" ON "prediction_bet" USING btree ("user_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "prediction_bet_question_id_idx" ON "prediction_bet" USING btree ("question_id");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "prediction_bet_user_question_idx" ON "prediction_bet" USING btree ("user_id","question_id");--> statement-breakpoint diff --git a/website/drizzle/0001_yummy_meggan.sql b/website/drizzle/0001_yummy_meggan.sql deleted file mode 100644 index d87e95a..0000000 --- a/website/drizzle/0001_yummy_meggan.sql +++ /dev/null @@ -1,15 +0,0 @@ -ALTER TYPE "transaction_type" ADD VALUE 'TRANSFER_IN';--> statement-breakpoint -ALTER TYPE "transaction_type" ADD VALUE 'TRANSFER_OUT';--> statement-breakpoint -ALTER TABLE "transaction" ADD COLUMN "recipient_user_id" integer;--> statement-breakpoint -ALTER TABLE "transaction" ADD COLUMN "sender_user_id" integer;--> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "transaction" ADD CONSTRAINT "transaction_recipient_user_id_user_id_fk" FOREIGN KEY ("recipient_user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "transaction" ADD CONSTRAINT "transaction_sender_user_id_user_id_fk" FOREIGN KEY ("sender_user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; diff --git a/website/drizzle/0002_lush_guardian.sql b/website/drizzle/0002_lush_guardian.sql deleted file mode 100644 index d7bccb3..0000000 --- a/website/drizzle/0002_lush_guardian.sql +++ /dev/null @@ -1,16 +0,0 @@ -ALTER TABLE "transaction" DROP CONSTRAINT "transaction_recipient_user_id_user_id_fk"; ---> statement-breakpoint -ALTER TABLE "transaction" DROP CONSTRAINT "transaction_sender_user_id_user_id_fk"; ---> statement-breakpoint -ALTER TABLE "coin" ALTER COLUMN "change_24h" SET DATA TYPE numeric(30, 4);--> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "transaction" ADD CONSTRAINT "transaction_recipient_user_id_user_id_fk" FOREIGN KEY ("recipient_user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "transaction" ADD CONSTRAINT "transaction_sender_user_id_user_id_fk" FOREIGN KEY ("sender_user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; diff --git a/website/drizzle/meta/0000_snapshot.json b/website/drizzle/meta/0000_snapshot.json index 52ae2ab..e66aff3 100644 --- a/website/drizzle/meta/0000_snapshot.json +++ b/website/drizzle/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "077132a0-ccad-4d56-855b-150b8fe31d94", + "id": "41f7bba3-1d5d-41ba-83bb-ca129ace81f0", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -292,7 +292,7 @@ }, "change_24h": { "name": "change_24h", - "type": "numeric(10, 4)", + "type": "numeric(30, 4)", "primaryKey": false, "notNull": false, "default": "'0.0000'" @@ -544,6 +544,136 @@ }, "uniqueConstraints": {} }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notification_user_id_idx": { + "name": "notification_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_type_idx": { + "name": "notification_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_is_read_idx": { + "name": "notification_is_read_idx", + "columns": [ + { + "expression": "is_read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_created_at_idx": { + "name": "notification_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_user_id_user_id_fk": { + "name": "notification_user_id_user_id_fk", + "tableFrom": "notification", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, "public.prediction_bet": { "name": "prediction_bet", "schema": "", @@ -1194,6 +1324,18 @@ "primaryKey": false, "notNull": true, "default": "now()" + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sender_user_id": { + "name": "sender_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false } }, "indexes": {}, @@ -1223,6 +1365,32 @@ ], "onDelete": "cascade", "onUpdate": "no action" + }, + "transaction_recipient_user_id_user_id_fk": { + "name": "transaction_recipient_user_id_user_id_fk", + "tableFrom": "transaction", + "tableTo": "user", + "columnsFrom": [ + "recipient_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "transaction_sender_user_id_user_id_fk": { + "name": "transaction_sender_user_id_user_id_fk", + "tableFrom": "transaction", + "tableTo": "user", + "columnsFrom": [ + "sender_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -1492,6 +1660,16 @@ } }, "enums": { + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "HOPIUM", + "SYSTEM", + "TRANSFER", + "RUG_PULL" + ] + }, "public.prediction_market_status": { "name": "prediction_market_status", "schema": "public", @@ -1506,7 +1684,9 @@ "schema": "public", "values": [ "BUY", - "SELL" + "SELL", + "TRANSFER_IN", + "TRANSFER_OUT" ] } }, diff --git a/website/drizzle/meta/0001_snapshot.json b/website/drizzle/meta/0001_snapshot.json deleted file mode 100644 index 4df2f2d..0000000 --- a/website/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,1559 +0,0 @@ -{ - "id": "5afb9188-6be0-462e-8455-2f1ac41dc67f", - "prevId": "077132a0-ccad-4d56-855b-150b8fe31d94", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.account": { - "name": "account", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id_token": { - "name": "id_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "account_user_id_user_id_fk": { - "name": "account_user_id_user_id_fk", - "tableFrom": "account", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.account_deletion_request": { - "name": "account_deletion_request", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "requested_at": { - "name": "requested_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "scheduled_deletion_at": { - "name": "scheduled_deletion_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "reason": { - "name": "reason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "is_processed": { - "name": "is_processed", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "account_deletion_request_user_id_idx": { - "name": "account_deletion_request_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "account_deletion_request_scheduled_deletion_idx": { - "name": "account_deletion_request_scheduled_deletion_idx", - "columns": [ - { - "expression": "scheduled_deletion_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "account_deletion_request_open_idx": { - "name": "account_deletion_request_open_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "is_processed = false", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "account_deletion_request_user_id_user_id_fk": { - "name": "account_deletion_request_user_id_user_id_fk", - "tableFrom": "account_deletion_request", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "account_deletion_request_user_id_unique": { - "name": "account_deletion_request_user_id_unique", - "nullsNotDistinct": false, - "columns": [ - "user_id" - ] - } - } - }, - "public.coin": { - "name": "coin", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "symbol": { - "name": "symbol", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "icon": { - "name": "icon", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "creator_id": { - "name": "creator_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "initial_supply": { - "name": "initial_supply", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true - }, - "circulating_supply": { - "name": "circulating_supply", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true - }, - "current_price": { - "name": "current_price", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "market_cap": { - "name": "market_cap", - "type": "numeric(30, 2)", - "primaryKey": false, - "notNull": true - }, - "volume_24h": { - "name": "volume_24h", - "type": "numeric(30, 2)", - "primaryKey": false, - "notNull": false, - "default": "'0.00'" - }, - "change_24h": { - "name": "change_24h", - "type": "numeric(10, 4)", - "primaryKey": false, - "notNull": false, - "default": "'0.0000'" - }, - "pool_coin_amount": { - "name": "pool_coin_amount", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true, - "default": "'0.00000000'" - }, - "pool_base_currency_amount": { - "name": "pool_base_currency_amount", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true, - "default": "'0.00000000'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "is_listed": { - "name": "is_listed", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - } - }, - "indexes": {}, - "foreignKeys": { - "coin_creator_id_user_id_fk": { - "name": "coin_creator_id_user_id_fk", - "tableFrom": "coin", - "tableTo": "user", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "coin_symbol_unique": { - "name": "coin_symbol_unique", - "nullsNotDistinct": false, - "columns": [ - "symbol" - ] - } - } - }, - "public.comment": { - "name": "comment", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "coin_id": { - "name": "coin_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "varchar(500)", - "primaryKey": false, - "notNull": true - }, - "likes_count": { - "name": "likes_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "is_deleted": { - "name": "is_deleted", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "comment_user_id_idx": { - "name": "comment_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "comment_coin_id_idx": { - "name": "comment_coin_id_idx", - "columns": [ - { - "expression": "coin_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "comment_user_id_user_id_fk": { - "name": "comment_user_id_user_id_fk", - "tableFrom": "comment", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "comment_coin_id_coin_id_fk": { - "name": "comment_coin_id_coin_id_fk", - "tableFrom": "comment", - "tableTo": "coin", - "columnsFrom": [ - "coin_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.comment_like": { - "name": "comment_like", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "comment_id": { - "name": "comment_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "comment_like_user_id_user_id_fk": { - "name": "comment_like_user_id_user_id_fk", - "tableFrom": "comment_like", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "comment_like_comment_id_comment_id_fk": { - "name": "comment_like_comment_id_comment_id_fk", - "tableFrom": "comment_like", - "tableTo": "comment", - "columnsFrom": [ - "comment_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "comment_like_user_id_comment_id_pk": { - "name": "comment_like_user_id_comment_id_pk", - "columns": [ - "user_id", - "comment_id" - ] - } - }, - "uniqueConstraints": {} - }, - "public.prediction_bet": { - "name": "prediction_bet", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "question_id": { - "name": "question_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "side": { - "name": "side", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "amount": { - "name": "amount", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "actual_winnings": { - "name": "actual_winnings", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "settled_at": { - "name": "settled_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "prediction_bet_user_id_idx": { - "name": "prediction_bet_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "prediction_bet_question_id_idx": { - "name": "prediction_bet_question_id_idx", - "columns": [ - { - "expression": "question_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "prediction_bet_user_question_idx": { - "name": "prediction_bet_user_question_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "question_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "prediction_bet_created_at_idx": { - "name": "prediction_bet_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "prediction_bet_user_id_user_id_fk": { - "name": "prediction_bet_user_id_user_id_fk", - "tableFrom": "prediction_bet", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "prediction_bet_question_id_prediction_question_id_fk": { - "name": "prediction_bet_question_id_prediction_question_id_fk", - "tableFrom": "prediction_bet", - "tableTo": "prediction_question", - "columnsFrom": [ - "question_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.prediction_question": { - "name": "prediction_question", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "creator_id": { - "name": "creator_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "question": { - "name": "question", - "type": "varchar(200)", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "prediction_market_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "resolution_date": { - "name": "resolution_date", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "ai_resolution": { - "name": "ai_resolution", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "total_yes_amount": { - "name": "total_yes_amount", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true, - "default": "'0.00000000'" - }, - "total_no_amount": { - "name": "total_no_amount", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true, - "default": "'0.00000000'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "resolved_at": { - "name": "resolved_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "requires_web_search": { - "name": "requires_web_search", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "validation_reason": { - "name": "validation_reason", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "prediction_question_creator_id_idx": { - "name": "prediction_question_creator_id_idx", - "columns": [ - { - "expression": "creator_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "prediction_question_status_idx": { - "name": "prediction_question_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "prediction_question_resolution_date_idx": { - "name": "prediction_question_resolution_date_idx", - "columns": [ - { - "expression": "resolution_date", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "prediction_question_creator_id_user_id_fk": { - "name": "prediction_question_creator_id_user_id_fk", - "tableFrom": "prediction_question", - "tableTo": "user", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.price_history": { - "name": "price_history", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "coin_id": { - "name": "coin_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "price": { - "name": "price", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "timestamp": { - "name": "timestamp", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "price_history_coin_id_coin_id_fk": { - "name": "price_history_coin_id_coin_id_fk", - "tableFrom": "price_history", - "tableTo": "coin", - "columnsFrom": [ - "coin_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.promo_code": { - "name": "promo_code", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "code": { - "name": "code", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "reward_amount": { - "name": "reward_amount", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "max_uses": { - "name": "max_uses", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_by": { - "name": "created_by", - "type": "integer", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "promo_code_created_by_user_id_fk": { - "name": "promo_code_created_by_user_id_fk", - "tableFrom": "promo_code", - "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "promo_code_code_unique": { - "name": "promo_code_code_unique", - "nullsNotDistinct": false, - "columns": [ - "code" - ] - } - } - }, - "public.promo_code_redemption": { - "name": "promo_code_redemption", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "promo_code_id": { - "name": "promo_code_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "reward_amount": { - "name": "reward_amount", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "redeemed_at": { - "name": "redeemed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "promo_code_redemption_user_id_user_id_fk": { - "name": "promo_code_redemption_user_id_user_id_fk", - "tableFrom": "promo_code_redemption", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "promo_code_redemption_promo_code_id_promo_code_id_fk": { - "name": "promo_code_redemption_promo_code_id_promo_code_id_fk", - "tableFrom": "promo_code_redemption", - "tableTo": "promo_code", - "columnsFrom": [ - "promo_code_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "promo_code_redemption_user_id_promo_code_id_unique": { - "name": "promo_code_redemption_user_id_promo_code_id_unique", - "nullsNotDistinct": false, - "columns": [ - "user_id", - "promo_code_id" - ] - } - } - }, - "public.session": { - "name": "session", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "session_user_id_user_id_fk": { - "name": "session_user_id_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "session_token_unique": { - "name": "session_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - } - }, - "public.transaction": { - "name": "transaction", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "coin_id": { - "name": "coin_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "transaction_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "quantity": { - "name": "quantity", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true - }, - "price_per_coin": { - "name": "price_per_coin", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "total_base_currency_amount": { - "name": "total_base_currency_amount", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true - }, - "timestamp": { - "name": "timestamp", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "recipient_user_id": { - "name": "recipient_user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "sender_user_id": { - "name": "sender_user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "transaction_user_id_user_id_fk": { - "name": "transaction_user_id_user_id_fk", - "tableFrom": "transaction", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "transaction_coin_id_coin_id_fk": { - "name": "transaction_coin_id_coin_id_fk", - "tableFrom": "transaction", - "tableTo": "coin", - "columnsFrom": [ - "coin_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "transaction_recipient_user_id_user_id_fk": { - "name": "transaction_recipient_user_id_user_id_fk", - "tableFrom": "transaction", - "tableTo": "user", - "columnsFrom": [ - "recipient_user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "transaction_sender_user_id_user_id_fk": { - "name": "transaction_sender_user_id_user_id_fk", - "tableFrom": "transaction", - "tableTo": "user", - "columnsFrom": [ - "sender_user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "is_admin": { - "name": "is_admin", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "is_banned": { - "name": "is_banned", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "ban_reason": { - "name": "ban_reason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "base_currency_balance": { - "name": "base_currency_balance", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true, - "default": "'100.00000000'" - }, - "bio": { - "name": "bio", - "type": "varchar(160)", - "primaryKey": false, - "notNull": false, - "default": "'Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”'" - }, - "username": { - "name": "username", - "type": "varchar(30)", - "primaryKey": false, - "notNull": true - }, - "volume_master": { - "name": "volume_master", - "type": "numeric(3, 2)", - "primaryKey": false, - "notNull": true, - "default": "'0.70'" - }, - "volume_muted": { - "name": "volume_muted", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "last_reward_claim": { - "name": "last_reward_claim", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "total_rewards_claimed": { - "name": "total_rewards_claimed", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true, - "default": "'0.00000000'" - }, - "login_streak": { - "name": "login_streak", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - }, - "user_username_unique": { - "name": "user_username_unique", - "nullsNotDistinct": false, - "columns": [ - "username" - ] - } - } - }, - "public.user_portfolio": { - "name": "user_portfolio", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "coin_id": { - "name": "coin_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "quantity": { - "name": "quantity", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "user_portfolio_user_id_user_id_fk": { - "name": "user_portfolio_user_id_user_id_fk", - "tableFrom": "user_portfolio", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "user_portfolio_coin_id_coin_id_fk": { - "name": "user_portfolio_coin_id_coin_id_fk", - "tableFrom": "user_portfolio", - "tableTo": "coin", - "columnsFrom": [ - "coin_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "user_portfolio_user_id_coin_id_pk": { - "name": "user_portfolio_user_id_coin_id_pk", - "columns": [ - "user_id", - "coin_id" - ] - } - }, - "uniqueConstraints": {} - }, - "public.verification": { - "name": "verification", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - } - }, - "enums": { - "public.prediction_market_status": { - "name": "prediction_market_status", - "schema": "public", - "values": [ - "ACTIVE", - "RESOLVED", - "CANCELLED" - ] - }, - "public.transaction_type": { - "name": "transaction_type", - "schema": "public", - "values": [ - "BUY", - "SELL", - "TRANSFER_IN", - "TRANSFER_OUT" - ] - } - }, - "schemas": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/website/drizzle/meta/0002_snapshot.json b/website/drizzle/meta/0002_snapshot.json deleted file mode 100644 index dfc98ae..0000000 --- a/website/drizzle/meta/0002_snapshot.json +++ /dev/null @@ -1,1559 +0,0 @@ -{ - "id": "42dd73ba-ecbe-4b72-aa93-3bfbfee8f534", - "prevId": "5afb9188-6be0-462e-8455-2f1ac41dc67f", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.account": { - "name": "account", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id_token": { - "name": "id_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "account_user_id_user_id_fk": { - "name": "account_user_id_user_id_fk", - "tableFrom": "account", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.account_deletion_request": { - "name": "account_deletion_request", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "requested_at": { - "name": "requested_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "scheduled_deletion_at": { - "name": "scheduled_deletion_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "reason": { - "name": "reason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "is_processed": { - "name": "is_processed", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "account_deletion_request_user_id_idx": { - "name": "account_deletion_request_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "account_deletion_request_scheduled_deletion_idx": { - "name": "account_deletion_request_scheduled_deletion_idx", - "columns": [ - { - "expression": "scheduled_deletion_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "account_deletion_request_open_idx": { - "name": "account_deletion_request_open_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "is_processed = false", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "account_deletion_request_user_id_user_id_fk": { - "name": "account_deletion_request_user_id_user_id_fk", - "tableFrom": "account_deletion_request", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "account_deletion_request_user_id_unique": { - "name": "account_deletion_request_user_id_unique", - "nullsNotDistinct": false, - "columns": [ - "user_id" - ] - } - } - }, - "public.coin": { - "name": "coin", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "symbol": { - "name": "symbol", - "type": "varchar(10)", - "primaryKey": false, - "notNull": true - }, - "icon": { - "name": "icon", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "creator_id": { - "name": "creator_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "initial_supply": { - "name": "initial_supply", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true - }, - "circulating_supply": { - "name": "circulating_supply", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true - }, - "current_price": { - "name": "current_price", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "market_cap": { - "name": "market_cap", - "type": "numeric(30, 2)", - "primaryKey": false, - "notNull": true - }, - "volume_24h": { - "name": "volume_24h", - "type": "numeric(30, 2)", - "primaryKey": false, - "notNull": false, - "default": "'0.00'" - }, - "change_24h": { - "name": "change_24h", - "type": "numeric(30, 4)", - "primaryKey": false, - "notNull": false, - "default": "'0.0000'" - }, - "pool_coin_amount": { - "name": "pool_coin_amount", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true, - "default": "'0.00000000'" - }, - "pool_base_currency_amount": { - "name": "pool_base_currency_amount", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true, - "default": "'0.00000000'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "is_listed": { - "name": "is_listed", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - } - }, - "indexes": {}, - "foreignKeys": { - "coin_creator_id_user_id_fk": { - "name": "coin_creator_id_user_id_fk", - "tableFrom": "coin", - "tableTo": "user", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "coin_symbol_unique": { - "name": "coin_symbol_unique", - "nullsNotDistinct": false, - "columns": [ - "symbol" - ] - } - } - }, - "public.comment": { - "name": "comment", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "coin_id": { - "name": "coin_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "varchar(500)", - "primaryKey": false, - "notNull": true - }, - "likes_count": { - "name": "likes_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "is_deleted": { - "name": "is_deleted", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "comment_user_id_idx": { - "name": "comment_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "comment_coin_id_idx": { - "name": "comment_coin_id_idx", - "columns": [ - { - "expression": "coin_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "comment_user_id_user_id_fk": { - "name": "comment_user_id_user_id_fk", - "tableFrom": "comment", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "comment_coin_id_coin_id_fk": { - "name": "comment_coin_id_coin_id_fk", - "tableFrom": "comment", - "tableTo": "coin", - "columnsFrom": [ - "coin_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.comment_like": { - "name": "comment_like", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "comment_id": { - "name": "comment_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "comment_like_user_id_user_id_fk": { - "name": "comment_like_user_id_user_id_fk", - "tableFrom": "comment_like", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "comment_like_comment_id_comment_id_fk": { - "name": "comment_like_comment_id_comment_id_fk", - "tableFrom": "comment_like", - "tableTo": "comment", - "columnsFrom": [ - "comment_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "comment_like_user_id_comment_id_pk": { - "name": "comment_like_user_id_comment_id_pk", - "columns": [ - "user_id", - "comment_id" - ] - } - }, - "uniqueConstraints": {} - }, - "public.prediction_bet": { - "name": "prediction_bet", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "question_id": { - "name": "question_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "side": { - "name": "side", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "amount": { - "name": "amount", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "actual_winnings": { - "name": "actual_winnings", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "settled_at": { - "name": "settled_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "prediction_bet_user_id_idx": { - "name": "prediction_bet_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "prediction_bet_question_id_idx": { - "name": "prediction_bet_question_id_idx", - "columns": [ - { - "expression": "question_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "prediction_bet_user_question_idx": { - "name": "prediction_bet_user_question_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "question_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "prediction_bet_created_at_idx": { - "name": "prediction_bet_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "prediction_bet_user_id_user_id_fk": { - "name": "prediction_bet_user_id_user_id_fk", - "tableFrom": "prediction_bet", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "prediction_bet_question_id_prediction_question_id_fk": { - "name": "prediction_bet_question_id_prediction_question_id_fk", - "tableFrom": "prediction_bet", - "tableTo": "prediction_question", - "columnsFrom": [ - "question_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.prediction_question": { - "name": "prediction_question", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "creator_id": { - "name": "creator_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "question": { - "name": "question", - "type": "varchar(200)", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "prediction_market_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "resolution_date": { - "name": "resolution_date", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "ai_resolution": { - "name": "ai_resolution", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "total_yes_amount": { - "name": "total_yes_amount", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true, - "default": "'0.00000000'" - }, - "total_no_amount": { - "name": "total_no_amount", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true, - "default": "'0.00000000'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "resolved_at": { - "name": "resolved_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "requires_web_search": { - "name": "requires_web_search", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "validation_reason": { - "name": "validation_reason", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "prediction_question_creator_id_idx": { - "name": "prediction_question_creator_id_idx", - "columns": [ - { - "expression": "creator_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "prediction_question_status_idx": { - "name": "prediction_question_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "prediction_question_resolution_date_idx": { - "name": "prediction_question_resolution_date_idx", - "columns": [ - { - "expression": "resolution_date", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "prediction_question_creator_id_user_id_fk": { - "name": "prediction_question_creator_id_user_id_fk", - "tableFrom": "prediction_question", - "tableTo": "user", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.price_history": { - "name": "price_history", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "coin_id": { - "name": "coin_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "price": { - "name": "price", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "timestamp": { - "name": "timestamp", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "price_history_coin_id_coin_id_fk": { - "name": "price_history_coin_id_coin_id_fk", - "tableFrom": "price_history", - "tableTo": "coin", - "columnsFrom": [ - "coin_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.promo_code": { - "name": "promo_code", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "code": { - "name": "code", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "reward_amount": { - "name": "reward_amount", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "max_uses": { - "name": "max_uses", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_by": { - "name": "created_by", - "type": "integer", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "promo_code_created_by_user_id_fk": { - "name": "promo_code_created_by_user_id_fk", - "tableFrom": "promo_code", - "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "promo_code_code_unique": { - "name": "promo_code_code_unique", - "nullsNotDistinct": false, - "columns": [ - "code" - ] - } - } - }, - "public.promo_code_redemption": { - "name": "promo_code_redemption", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "promo_code_id": { - "name": "promo_code_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "reward_amount": { - "name": "reward_amount", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "redeemed_at": { - "name": "redeemed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "promo_code_redemption_user_id_user_id_fk": { - "name": "promo_code_redemption_user_id_user_id_fk", - "tableFrom": "promo_code_redemption", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "promo_code_redemption_promo_code_id_promo_code_id_fk": { - "name": "promo_code_redemption_promo_code_id_promo_code_id_fk", - "tableFrom": "promo_code_redemption", - "tableTo": "promo_code", - "columnsFrom": [ - "promo_code_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "promo_code_redemption_user_id_promo_code_id_unique": { - "name": "promo_code_redemption_user_id_promo_code_id_unique", - "nullsNotDistinct": false, - "columns": [ - "user_id", - "promo_code_id" - ] - } - } - }, - "public.session": { - "name": "session", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "session_user_id_user_id_fk": { - "name": "session_user_id_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "session_token_unique": { - "name": "session_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - } - }, - "public.transaction": { - "name": "transaction", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "coin_id": { - "name": "coin_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "transaction_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "quantity": { - "name": "quantity", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true - }, - "price_per_coin": { - "name": "price_per_coin", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true - }, - "total_base_currency_amount": { - "name": "total_base_currency_amount", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true - }, - "timestamp": { - "name": "timestamp", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "recipient_user_id": { - "name": "recipient_user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "sender_user_id": { - "name": "sender_user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "transaction_user_id_user_id_fk": { - "name": "transaction_user_id_user_id_fk", - "tableFrom": "transaction", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "transaction_coin_id_coin_id_fk": { - "name": "transaction_coin_id_coin_id_fk", - "tableFrom": "transaction", - "tableTo": "coin", - "columnsFrom": [ - "coin_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "transaction_recipient_user_id_user_id_fk": { - "name": "transaction_recipient_user_id_user_id_fk", - "tableFrom": "transaction", - "tableTo": "user", - "columnsFrom": [ - "recipient_user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "transaction_sender_user_id_user_id_fk": { - "name": "transaction_sender_user_id_user_id_fk", - "tableFrom": "transaction", - "tableTo": "user", - "columnsFrom": [ - "sender_user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "is_admin": { - "name": "is_admin", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "is_banned": { - "name": "is_banned", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "ban_reason": { - "name": "ban_reason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "base_currency_balance": { - "name": "base_currency_balance", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true, - "default": "'100.00000000'" - }, - "bio": { - "name": "bio", - "type": "varchar(160)", - "primaryKey": false, - "notNull": false, - "default": "'Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”'" - }, - "username": { - "name": "username", - "type": "varchar(30)", - "primaryKey": false, - "notNull": true - }, - "volume_master": { - "name": "volume_master", - "type": "numeric(3, 2)", - "primaryKey": false, - "notNull": true, - "default": "'0.70'" - }, - "volume_muted": { - "name": "volume_muted", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "last_reward_claim": { - "name": "last_reward_claim", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "total_rewards_claimed": { - "name": "total_rewards_claimed", - "type": "numeric(20, 8)", - "primaryKey": false, - "notNull": true, - "default": "'0.00000000'" - }, - "login_streak": { - "name": "login_streak", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - }, - "user_username_unique": { - "name": "user_username_unique", - "nullsNotDistinct": false, - "columns": [ - "username" - ] - } - } - }, - "public.user_portfolio": { - "name": "user_portfolio", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "coin_id": { - "name": "coin_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "quantity": { - "name": "quantity", - "type": "numeric(30, 8)", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "user_portfolio_user_id_user_id_fk": { - "name": "user_portfolio_user_id_user_id_fk", - "tableFrom": "user_portfolio", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "user_portfolio_coin_id_coin_id_fk": { - "name": "user_portfolio_coin_id_coin_id_fk", - "tableFrom": "user_portfolio", - "tableTo": "coin", - "columnsFrom": [ - "coin_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "user_portfolio_user_id_coin_id_pk": { - "name": "user_portfolio_user_id_coin_id_pk", - "columns": [ - "user_id", - "coin_id" - ] - } - }, - "uniqueConstraints": {} - }, - "public.verification": { - "name": "verification", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - } - }, - "enums": { - "public.prediction_market_status": { - "name": "prediction_market_status", - "schema": "public", - "values": [ - "ACTIVE", - "RESOLVED", - "CANCELLED" - ] - }, - "public.transaction_type": { - "name": "transaction_type", - "schema": "public", - "values": [ - "BUY", - "SELL", - "TRANSFER_IN", - "TRANSFER_OUT" - ] - } - }, - "schemas": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/website/drizzle/meta/_journal.json b/website/drizzle/meta/_journal.json index 18066b9..ceefa6b 100644 --- a/website/drizzle/meta/_journal.json +++ b/website/drizzle/meta/_journal.json @@ -5,22 +5,8 @@ { "idx": 0, "version": "7", - "when": 1748604150899, - "tag": "0000_spooky_umar", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1748690470287, - "tag": "0001_yummy_meggan", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1748700252762, - "tag": "0002_lush_guardian", + "when": 1749654046953, + "tag": "0000_crazy_bloodstrike", "breakpoints": true } ] diff --git a/website/src/lib/components/self/AppSidebar.svelte b/website/src/lib/components/self/AppSidebar.svelte index d770360..d9917b4 100644 --- a/website/src/lib/components/self/AppSidebar.svelte +++ b/website/src/lib/components/self/AppSidebar.svelte @@ -30,7 +30,8 @@ ShieldCheck, Hammer, BookOpen, - Info + Info, + Bell } from 'lucide-svelte'; import { mode, setMode } from 'mode-watcher'; import type { HTMLAttributes } from 'svelte/elements'; @@ -46,6 +47,7 @@ import { goto } from '$app/navigation'; import { liveTradesStore, isLoadingTrades } from '$lib/stores/websocket'; import { onMount } from 'svelte'; + import { UNREAD_COUNT, fetchNotifications } from '$lib/stores/notifications'; const data = { navMain: [ @@ -57,6 +59,7 @@ { title: 'Portfolio', url: '/portfolio', icon: BriefcaseBusiness }, { title: 'Treemap', url: '/treemap', icon: ChartColumn }, { title: 'Create coin', url: '/coin/create', icon: Coins }, + { title: 'Notifications', url: '/notifications', icon: Bell }, { title: 'About', url: '/about', icon: Info } ] }; @@ -70,6 +73,7 @@ onMount(() => { if ($USER_DATA) { fetchPortfolioSummary(); + fetchNotifications(); } else { PORTFOLIO_SUMMARY.set(null); } @@ -177,10 +181,15 @@ handleNavClick(item.title)} - class={`${props.class}`} + class={`${props.class} ${item.title === 'Notifications' && !$USER_DATA ? 'pointer-events-none opacity-50' : ''}`} > {item.title} + {#if item.title === 'Notifications' && $UNREAD_COUNT > 0 && $USER_DATA} + + {$UNREAD_COUNT > 99 ? '99+' : $UNREAD_COUNT} + + {/if} {/snippet} @@ -358,7 +367,8 @@
Coins: - ${formatCurrency($PORTFOLIO_SUMMARY.totalCoinValue)} + ${formatCurrency($PORTFOLIO_SUMMARY.totalCoinValue)}
{/if} diff --git a/website/src/lib/components/self/skeletons/NotificationsSkeleton.svelte b/website/src/lib/components/self/skeletons/NotificationsSkeleton.svelte new file mode 100644 index 0000000..3fba32f --- /dev/null +++ b/website/src/lib/components/self/skeletons/NotificationsSkeleton.svelte @@ -0,0 +1,21 @@ + + +
+ {#each Array(8) as _, i} +
+ +
+ + +
+
+ +
+
+ {#if i < 7} +
+ {/if} + {/each} +
diff --git a/website/src/lib/server/db/schema.ts b/website/src/lib/server/db/schema.ts index 666d265..6fd02a6 100644 --- a/website/src/lib/server/db/schema.ts +++ b/website/src/lib/server/db/schema.ts @@ -3,6 +3,7 @@ import { sql } from "drizzle-orm"; export const transactionTypeEnum = pgEnum('transaction_type', ['BUY', 'SELL', 'TRANSFER_IN', 'TRANSFER_OUT']); export const predictionMarketEnum = pgEnum('prediction_market_status', ['ACTIVE', 'RESOLVED', 'CANCELLED']); +export const notificationTypeEnum = pgEnum('notification_type', ['HOPIUM', 'SYSTEM', 'TRANSFER', 'RUG_PULL']); export const user = pgTable("user", { id: serial("id").primaryKey(), @@ -226,4 +227,21 @@ export const accountDeletionRequest = pgTable("account_deletion_request", { .on(table.userId) .where(sql`is_processed = false`), }; +}); + +export const notifications = pgTable("notification", { + id: serial("id").primaryKey(), + userId: integer("user_id").notNull().references(() => user.id, { onDelete: "cascade" }), + type: notificationTypeEnum("type").notNull(), + title: varchar("title", { length: 200 }).notNull(), + message: text("message").notNull(), + isRead: boolean("is_read").notNull().default(false), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}, (table) => { + return { + userIdIdx: index("notification_user_id_idx").on(table.userId), + typeIdx: index("notification_type_idx").on(table.type), + isReadIdx: index("notification_is_read_idx").on(table.isRead), + createdAtIdx: index("notification_created_at_idx").on(table.createdAt), + }; }); \ No newline at end of file diff --git a/website/src/lib/server/job.ts b/website/src/lib/server/job.ts index d12e0d7..528d15e 100644 --- a/website/src/lib/server/job.ts +++ b/website/src/lib/server/job.ts @@ -2,6 +2,8 @@ import { db } from '$lib/server/db'; import { predictionQuestion, predictionBet, user, accountDeletionRequest, session, account, promoCodeRedemption, userPortfolio, commentLike, comment, transaction, coin } from '$lib/server/db/schema'; import { eq, and, lte, isNull } from 'drizzle-orm'; import { resolveQuestion, getRugplayData } from '$lib/server/ai'; +import { createNotification } from '$lib/server/notification'; +import { formatValue } from '$lib/utils'; export async function resolveExpiredQuestions() { const now = new Date(); @@ -68,6 +70,13 @@ export async function resolveExpiredQuestions() { ? Number(question.totalYesAmount) : Number(question.totalNoAmount); + const notificationsToCreate: Array<{ + userId: number; + amount: number; + winnings: number; + won: boolean; + }> = []; + for (const bet of bets) { const won = bet.side === resolution.resolution; @@ -101,6 +110,32 @@ export async function resolveExpiredQuestions() { .where(eq(user.id, bet.userId)); } } + + if (bet.userId !== null) { + notificationsToCreate.push({ + userId: bet.userId, + amount: Number(bet.amount), + winnings, + won + }); + } + } + + // Create notifications for all users who had bets + for (const notifData of notificationsToCreate) { + const { userId, amount, winnings, won } = notifData; + + const title = won ? 'Prediction won! 🎉' : 'Prediction lost ;('; + const message = won + ? `You won ${formatValue(winnings)} on "${question.question}"` + : `You lost ${formatValue(amount)} on "${question.question}"`; + + await createNotification( + userId.toString(), + 'HOPIUM', + title, + message, + ); } }); diff --git a/website/src/lib/server/notification.ts b/website/src/lib/server/notification.ts new file mode 100644 index 0000000..a5dd010 --- /dev/null +++ b/website/src/lib/server/notification.ts @@ -0,0 +1,36 @@ +import { db } from './db'; +import { notifications, notificationTypeEnum } from './db/schema'; +import { redis } from './redis'; + +export type NotificationType = typeof notificationTypeEnum.enumValues[number]; + +export async function createNotification( + userId: string, + type: NotificationType, + title: string, + message: string, +): Promise { + await db.insert(notifications).values({ + userId: parseInt(userId), + type, + title, + message + }); + + try { + const channel = `notifications:${userId}`; + + const payload = { + type: 'notification', + timestamp: new Date().toISOString(), + userId, + notificationType: type, + title, + message, + }; + + await redis.publish(channel, JSON.stringify(payload)); + } catch (error) { + console.error('Failed to send notification via Redis:', error); + } +} diff --git a/website/src/lib/stores/notifications.ts b/website/src/lib/stores/notifications.ts new file mode 100644 index 0000000..a5a00b9 --- /dev/null +++ b/website/src/lib/stores/notifications.ts @@ -0,0 +1,61 @@ +import { writable, derived } from 'svelte/store'; + +export interface Notification { + id: number; + type: string; + title: string; + message: string; + data: any; + isRead: boolean; + createdAt: string; +} + +export const NOTIFICATIONS = writable([]); +export const UNREAD_COUNT = writable(0); + +export async function fetchNotifications(unreadOnly = false) { + try { + const params = new URLSearchParams({ + unread_only: unreadOnly.toString() + }); + + const response = await fetch(`/api/notifications?${params}`); + if (!response.ok) throw new Error('Failed to fetch notifications'); + + const data = await response.json(); + + NOTIFICATIONS.set(data.notifications); + UNREAD_COUNT.set(data.unreadCount); + + return data; + } catch (error) { + console.error('Failed to fetch notifications:', error); + throw error; + } +} + +export async function markNotificationsAsRead(ids: number[]) { + try { + const response = await fetch('/api/notifications', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ids, markAsRead: true }) + }); + + if (!response.ok) throw new Error('Failed to mark notifications as read'); + + NOTIFICATIONS.update(notifications => + notifications.map(notif => + ids.includes(notif.id) ? { ...notif, isRead: true } : notif + ) + ); + + UNREAD_COUNT.update(count => Math.max(0, count - ids.length)); + + } catch (error) { + console.error('Failed to mark notifications as read:', error); + throw error; + } +} + +export const hasUnreadNotifications = derived(UNREAD_COUNT, count => count > 0); diff --git a/website/src/lib/stores/websocket.ts b/website/src/lib/stores/websocket.ts index 4a2b8e8..eb34795 100644 --- a/website/src/lib/stores/websocket.ts +++ b/website/src/lib/stores/websocket.ts @@ -1,6 +1,10 @@ import { writable } from 'svelte/store'; import { browser } from '$app/environment'; import { PUBLIC_WEBSOCKET_URL } from '$env/static/public'; +import { NOTIFICATIONS, UNREAD_COUNT } from './notifications'; +import { USER_DATA } from './user-data'; +import { toast } from 'svelte-sonner'; +import { goto } from '$app/navigation'; export interface LiveTrade { type: 'BUY' | 'SELL' | 'TRANSFER_IN' | 'TRANSFER_OUT'; @@ -187,6 +191,32 @@ function handleWebSocketMessage(event: MessageEvent): void { handleCommentMessage(message); break; + case 'notification': + const notification = { + id: Date.now(), + type: message.notificationType, + title: message.title, + message: message.message, + isRead: false, + createdAt: message.timestamp, + data: message.amount ? { amount: message.amount } : null + }; + + NOTIFICATIONS.update(notifications => [notification, ...notifications]); + UNREAD_COUNT.update(count => count + 1); + + toast.success(message.title, { + description: message.message, + action: { + label: 'View', + onClick: () => { + goto('/notifications'); + } + }, + duration: 5000 + }); + break; + default: console.log('Unhandled message type:', message.type, message); } @@ -267,13 +297,56 @@ function unsubscribeFromPriceUpdates(coinSymbol: string): void { priceUpdateSubscriptions.delete(coinSymbol); } -export const websocketController = { - connect, - disconnect, - setCoin, - subscribeToComments, - unsubscribeFromComments, - subscribeToPriceUpdates, - unsubscribeFromPriceUpdates, - loadInitialTrades -}; \ No newline at end of file +class WebSocketController { + connect() { + connect(); + } + + disconnect() { + disconnect(); + } + + setCoin(coinSymbol: string) { + setCoin(coinSymbol); + } + + subscribeToComments(coinSymbol: string, callback: (message: any) => void) { + subscribeToComments(coinSymbol, callback); + } + + unsubscribeFromComments(coinSymbol: string) { + unsubscribeFromComments(coinSymbol); + } + + subscribeToPriceUpdates(coinSymbol: string, callback: (priceUpdate: PriceUpdate) => void) { + subscribeToPriceUpdates(coinSymbol, callback); + } + + unsubscribeFromPriceUpdates(coinSymbol: string) { + unsubscribeFromPriceUpdates(coinSymbol); + } + + loadInitialTrades(mode: 'preview' | 'expanded' = 'preview') { + loadInitialTrades(mode); + } + + setUser(userId: string) { + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ + type: 'set_user', + userId + })); + } + } +} + +// Auto-connect user when USER_DATA changes +if (typeof window !== 'undefined') { + USER_DATA.subscribe(user => { + if (user?.id) { + websocketController.setUser(user.id.toString()); + } + }); +} + +export const websocketController = new WebSocketController(); \ No newline at end of file diff --git a/website/src/routes/api/coin/[coinSymbol]/trade/+server.ts b/website/src/routes/api/coin/[coinSymbol]/trade/+server.ts index 0696cea..821e93e 100644 --- a/website/src/routes/api/coin/[coinSymbol]/trade/+server.ts +++ b/website/src/routes/api/coin/[coinSymbol]/trade/+server.ts @@ -4,6 +4,7 @@ import { db } from '$lib/server/db'; import { coin, userPortfolio, user, transaction, priceHistory } from '$lib/server/db/schema'; import { eq, and, gte } from 'drizzle-orm'; import { redis } from '$lib/server/redis'; +import { createNotification } from '$lib/server/notification'; async function calculate24hMetrics(coinId: number, currentPrice: number) { const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); @@ -329,6 +330,36 @@ export async function POST({ params, request }) { }) .where(eq(coin.id, coinData.id)); + const isRugPull = priceImpact < -20 && totalCost > 1000; + + // Send rug pull notifications to affected users + if (isRugPull) { + (async () => { + const affectedUsers = await db + .select({ + userId: userPortfolio.userId, + quantity: userPortfolio.quantity + }) + .from(userPortfolio) + .where(eq(userPortfolio.coinId, coinData.id)); + + for (const holder of affectedUsers) { + if (holder.userId === userId) continue; + + const holdingValue = Number(holder.quantity) * newPrice; + if (holdingValue > 10) { + const lossAmount = Number(holder.quantity) * (currentPrice - newPrice); + await createNotification( + holder.userId.toString(), + 'RUG_PULL', + 'Coin rugpulled!', + `A coin you owned, ${coinData.name} (*${normalizedSymbol}), crashed ${Math.abs(priceImpact).toFixed(1)}%!`, + ); + } + } + })(); + } + const priceUpdateData = { currentPrice: newPrice, marketCap: Number(coinData.circulatingSupply) * newPrice, diff --git a/website/src/routes/api/notifications/+server.ts b/website/src/routes/api/notifications/+server.ts new file mode 100644 index 0000000..1438ef1 --- /dev/null +++ b/website/src/routes/api/notifications/+server.ts @@ -0,0 +1,77 @@ +import { auth } from '$lib/auth'; +import { error, json } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { notifications } from '$lib/server/db/schema'; +import { eq, desc, and, count, inArray } from 'drizzle-orm'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ url, request }) => { + const session = await auth.api.getSession({ headers: request.headers }); + if (!session?.user) throw error(401, 'Not authenticated'); + + const userId = Number(session.user.id); + const unreadOnly = url.searchParams.get('unread_only') === 'true'; + + try { + const conditions = [eq(notifications.userId, userId)]; + if (unreadOnly) { + conditions.push(eq(notifications.isRead, false)); + } + + const whereCondition = and(...conditions); + + const notificationsList = await db.select({ + id: notifications.id, + type: notifications.type, + title: notifications.title, + message: notifications.message, + isRead: notifications.isRead, + createdAt: notifications.createdAt, + }) + .from(notifications) + .where(whereCondition) + .orderBy(desc(notifications.createdAt)) + .limit(50); + + const unreadCount = await db + .select({ count: count() }) + .from(notifications) + .where(and(eq(notifications.userId, userId), eq(notifications.isRead, false))) + .then(result => result[0]?.count || 0); + + return json({ + notifications: notificationsList, + unreadCount + }); + } catch (e) { + console.error('Failed to fetch notificationss:', e); + throw error(500, 'Failed to fetch notificationss'); + } +}; + +export const PATCH: RequestHandler = async ({ request }) => { + const session = await auth.api.getSession({ headers: request.headers }); + if (!session?.user) throw error(401, 'Not authenticated'); + + const userId = Number(session.user.id); + const { ids, markAsRead } = await request.json(); + + if (!Array.isArray(ids) || typeof markAsRead !== 'boolean') { + throw error(400, 'Invalid request body'); + } + + try { + await db + .update(notifications) + .set({ isRead: markAsRead }) + .where(and( + eq(notifications.userId, userId), + inArray(notifications.id, ids) + )); + + return json({ success: true }); + } catch (e) { + console.error('Failed to update notifications:', e); + throw error(500, 'Failed to update notifications'); + } +}; diff --git a/website/src/routes/api/transfer/+server.ts b/website/src/routes/api/transfer/+server.ts index e9ce233..726d345 100644 --- a/website/src/routes/api/transfer/+server.ts +++ b/website/src/routes/api/transfer/+server.ts @@ -3,6 +3,8 @@ import { error, json } from '@sveltejs/kit'; import { db } from '$lib/server/db'; import { user, userPortfolio, coin, transaction } from '$lib/server/db/schema'; import { eq, and } from 'drizzle-orm'; +import { createNotification } from '$lib/server/notification'; +import { formatValue } from '$lib/utils'; import type { RequestHandler } from './$types'; interface TransferRequest { @@ -19,7 +21,7 @@ export const POST: RequestHandler = async ({ request }) => { if (!session?.user) { throw error(401, 'Not authenticated'); - } try { + } try { const { recipientUsername, type, amount, coinSymbol }: TransferRequest = await request.json(); if (!recipientUsername || !type || !amount || typeof amount !== 'number' || !Number.isFinite(amount) || amount <= 0) { @@ -123,6 +125,15 @@ export const POST: RequestHandler = async ({ request }) => { recipientUserId: recipientData.id }); + (async () => { + await createNotification( + recipientData.id.toString(), + 'TRANSFER', + 'Money received!', + `You received ${formatValue(amount)} from @${senderData.username}`, + ); + })(); + return json({ success: true, type: 'CASH', @@ -247,6 +258,15 @@ export const POST: RequestHandler = async ({ request }) => { recipientUserId: recipientData.id }); + (async () => { + await createNotification( + recipientData.id.toString(), + 'TRANSFER', + 'Coins received!', + `You received ${amount.toFixed(6)} *${coinData.symbol} from @${senderData.username}`, + ); + })(); + return json({ success: true, type: 'COIN', diff --git a/website/src/routes/notifications/+page.svelte b/website/src/routes/notifications/+page.svelte new file mode 100644 index 0000000..7a8ba6a --- /dev/null +++ b/website/src/routes/notifications/+page.svelte @@ -0,0 +1,202 @@ + + + + +
+
+
+

Notifications

+

Stay updated with your activities

+
+
+ + + + {#if !$USER_DATA} +
+
+ +
+

Please sign in

+

You need to be signed in to view notifications

+
+ {:else if loading} + + {:else if !$NOTIFICATIONS || $NOTIFICATIONS.length === 0} +
+
+ +
+

No notifications yet

+

You'll see updates about your activities here

+
+ {:else} + +
+ {#each $NOTIFICATIONS as notification, index (notification.id)} + {@const IconComponent = getNotificationIcon(notification.type)} + {@const isNewNotification = newNotificationIds.includes(notification.id)} + + + {#if index < $NOTIFICATIONS.length - 1} + + {/if} + {/each} +
+
+ {/if} +
+
+
diff --git a/website/websocket/src/main.ts b/website/websocket/src/main.ts index fd6a58f..bd3984c 100644 --- a/website/websocket/src/main.ts +++ b/website/websocket/src/main.ts @@ -14,10 +14,22 @@ if (!process.env.REDIS_URL) { const redis = new Redis(process.env.REDIS_URL); +const HEARTBEAT_INTERVAL = 30_000; + +type WebSocketData = { + coinSymbol?: string; + userId?: string; + lastActivity: number; +}; + +const coinSockets = new Map>>(); +const userSockets = new Map>>(); +const pingIntervals = new WeakMap, NodeJS.Timeout>(); + redis.on('error', (err) => console.error('Redis Client Error', err)); redis.on('connect', () => { - redis.psubscribe('comments:*', 'prices:*', (err, count) => { + redis.psubscribe('comments:*', 'prices:*', 'notifications:*', (err, count) => { if (err) console.error("Failed to psubscribe to patterns", err); else console.log(`Successfully psubscribed to patterns. Active psubscriptions: ${count}`); }); @@ -59,6 +71,17 @@ redis.on('pmessage', (pattern, channel, msg) => { } } } + } else if (channel.startsWith('notifications:')) { + const userId = channel.substring('notifications:'.length); + const sockets = userSockets.get(userId); + console.log(`Received notification for user ${userId}:`, msg); + if (sockets) { + for (const ws of sockets) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(msg); + } + } + } } } catch (error) { console.error('Error processing Redis pmessage:', error, `Pattern: ${pattern}, Channel: ${channel}, Raw message: ${msg}`); @@ -85,16 +108,6 @@ redis.on('message', (channel, msg) => { } }); -const HEARTBEAT_INTERVAL = 30_000; - -type WebSocketData = { - coinSymbol?: string; - lastActivity: number; -}; - -const coinSockets = new Map>>(); -const pingIntervals = new WeakMap, NodeJS.Timeout>(); - function handleSetCoin(ws: ServerWebSocket, coinSymbol: string) { if (ws.data.coinSymbol) { const prev = coinSockets.get(ws.data.coinSymbol); @@ -115,6 +128,26 @@ function handleSetCoin(ws: ServerWebSocket, coinSymbol: string) { } } +function handleSetUser(ws: ServerWebSocket, userId: string) { + if (ws.data.userId) { + const prev = userSockets.get(ws.data.userId); + if (prev) { + prev.delete(ws); + if (prev.size === 0) { + userSockets.delete(ws.data.userId); + } + } + } + + ws.data.userId = userId; + + if (!userSockets.has(userId)) { + userSockets.set(userId, new Set([ws])); + } else { + userSockets.get(userId)!.add(ws); + } +} + function checkConnections() { const now = Date.now(); for (const [coinSymbol, sockets] of coinSockets.entries()) { @@ -163,10 +196,13 @@ const server = Bun.serve({ const data = JSON.parse(msg) as { type: string; coinSymbol?: string; + userId?: string; }; if (data.type === 'set_coin' && data.coinSymbol) { handleSetCoin(ws, data.coinSymbol); + } else if (data.type === 'set_user' && data.userId) { + handleSetUser(ws, data.userId); } else if (data.type === 'pong') { ws.data.lastActivity = Date.now(); } @@ -201,6 +237,16 @@ const server = Bun.serve({ } } } + + if (ws.data.userId) { + const sockets = userSockets.get(ws.data.userId); + if (sockets) { + sockets.delete(ws); + if (sockets.size === 0) { + userSockets.delete(ws.data.userId); + } + } + } } } });