diff --git a/drizzle.config.ts b/drizzle.config.ts index 41be97e..c9b7c07 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from 'drizzle-kit'; const databaseUrl = process.env.DATABASE_URL ?? 'postgresql://samreshu:samreshu_dev@localhost:5432/samreshu'; export default defineConfig({ - schema: './src/db/schema/index.ts', + schema: './src/db/schema/*.ts', out: './src/db/migrations', dialect: 'postgresql', dbCredentials: { diff --git a/package.json b/package.json index a905145..509708b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "tsc", "dev": "tsx watch src/server.ts", "start": "node dist/server.js", - "db:generate": "drizzle-kit generate --config=drizzle.config.ts", + "db:generate": "tsx node_modules/drizzle-kit/bin.cjs generate --config=drizzle.config.ts", "db:migrate": "tsx src/db/migrate.ts", "db:studio": "drizzle-kit studio", "db:seed": "tsx src/db/seed.ts", diff --git a/src/db/migrations/0000_fearless_salo.sql b/src/db/migrations/0000_fearless_salo.sql new file mode 100644 index 0000000..f97dab8 --- /dev/null +++ b/src/db/migrations/0000_fearless_salo.sql @@ -0,0 +1,246 @@ +CREATE TYPE "public"."level" AS ENUM('basic', 'beginner', 'intermediate', 'advanced', 'expert');--> statement-breakpoint +CREATE TYPE "public"."plan" AS ENUM('free', 'pro');--> statement-breakpoint +CREATE TYPE "public"."question_source" AS ENUM('llm_generated', 'manual');--> statement-breakpoint +CREATE TYPE "public"."question_status" AS ENUM('pending', 'approved', 'rejected');--> statement-breakpoint +CREATE TYPE "public"."question_type" AS ENUM('single_choice', 'multiple_select', 'true_false', 'short_text');--> statement-breakpoint +CREATE TYPE "public"."report_status" AS ENUM('open', 'resolved', 'dismissed');--> statement-breakpoint +CREATE TYPE "public"."self_level" AS ENUM('jun', 'mid', 'sen');--> statement-breakpoint +CREATE TYPE "public"."stack" AS ENUM('html', 'css', 'js', 'ts', 'react', 'vue', 'nodejs', 'git', 'web_basics');--> statement-breakpoint +CREATE TYPE "public"."subscription_status" AS ENUM('active', 'trialing', 'cancelled', 'expired');--> statement-breakpoint +CREATE TYPE "public"."test_mode" AS ENUM('fixed', 'infinite', 'marathon');--> statement-breakpoint +CREATE TYPE "public"."test_status" AS ENUM('in_progress', 'completed', 'abandoned');--> statement-breakpoint +CREATE TYPE "public"."user_role" AS ENUM('guest', 'free', 'pro', 'admin');--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "email" varchar(255) NOT NULL, + "password_hash" varchar(255) NOT NULL, + "nickname" varchar(30) NOT NULL, + "avatar_url" varchar(500), + "country" varchar(100), + "city" varchar(100), + "self_level" "self_level", + "is_public" boolean DEFAULT true NOT NULL, + "role" "user_role" DEFAULT 'free' NOT NULL, + "email_verified_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "users_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "sessions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "refresh_token_hash" varchar(255) NOT NULL, + "user_agent" varchar(500), + "ip_address" varchar(45), + "last_active_at" timestamp with time zone DEFAULT now() NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "subscriptions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "plan" "plan" NOT NULL, + "status" "subscription_status" NOT NULL, + "started_at" timestamp with time zone NOT NULL, + "expires_at" timestamp with time zone, + "cancelled_at" timestamp with time zone, + "payment_provider" varchar(50), + "external_id" varchar(255), + CONSTRAINT "subscriptions_user_id_unique" UNIQUE("user_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "tests" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "stack" "stack" NOT NULL, + "level" "level" NOT NULL, + "question_count" integer NOT NULL, + "mode" "test_mode" DEFAULT 'fixed' NOT NULL, + "status" "test_status" DEFAULT 'in_progress' NOT NULL, + "score" integer, + "started_at" timestamp with time zone DEFAULT now() NOT NULL, + "finished_at" timestamp with time zone, + "time_limit_seconds" integer +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "test_questions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "test_id" uuid NOT NULL, + "question_bank_id" uuid, + "order_number" integer NOT NULL, + "type" "question_type" NOT NULL, + "question_text" text NOT NULL, + "options" jsonb, + "correct_answer" jsonb NOT NULL, + "explanation" text NOT NULL, + "user_answer" jsonb, + "is_correct" boolean, + "answered_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "question_bank" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "stack" "stack" NOT NULL, + "level" "level" NOT NULL, + "type" "question_type" NOT NULL, + "question_text" text NOT NULL, + "options" jsonb, + "correct_answer" jsonb NOT NULL, + "explanation" text NOT NULL, + "status" "question_status" DEFAULT 'pending' NOT NULL, + "source" "question_source" NOT NULL, + "usage_count" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "approved_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "question_cache_meta" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "question_bank_id" uuid NOT NULL, + "llm_model" varchar(100) NOT NULL, + "prompt_hash" varchar(64) NOT NULL, + "generation_time_ms" integer NOT NULL, + "raw_response" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "question_reports" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "question_bank_id" uuid NOT NULL, + "user_id" uuid NOT NULL, + "reason" text NOT NULL, + "status" "report_status" DEFAULT 'open' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "resolved_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "user_stats" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "stack" "stack" NOT NULL, + "level" "level" NOT NULL, + "total_questions" integer DEFAULT 0 NOT NULL, + "correct_answers" integer DEFAULT 0 NOT NULL, + "tests_taken" integer DEFAULT 0 NOT NULL, + "last_test_at" timestamp with time zone, + CONSTRAINT "user_stats_user_id_stack_level_unique" UNIQUE("user_id","stack","level") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "audit_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "admin_id" uuid NOT NULL, + "action" varchar(100) NOT NULL, + "target_type" varchar(50) NOT NULL, + "target_id" uuid NOT NULL, + "details" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "user_question_log" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "question_bank_id" uuid NOT NULL, + "seen_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "email_verification_codes" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "code" varchar(10) NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "password_reset_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "token_hash" varchar(255) NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "tests" ADD CONSTRAINT "tests_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "test_questions" ADD CONSTRAINT "test_questions_test_id_tests_id_fk" FOREIGN KEY ("test_id") REFERENCES "public"."tests"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "test_questions" ADD CONSTRAINT "test_questions_question_bank_id_question_bank_id_fk" FOREIGN KEY ("question_bank_id") REFERENCES "public"."question_bank"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "question_cache_meta" ADD CONSTRAINT "question_cache_meta_question_bank_id_question_bank_id_fk" FOREIGN KEY ("question_bank_id") REFERENCES "public"."question_bank"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "question_reports" ADD CONSTRAINT "question_reports_question_bank_id_question_bank_id_fk" FOREIGN KEY ("question_bank_id") REFERENCES "public"."question_bank"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "question_reports" ADD CONSTRAINT "question_reports_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_stats" ADD CONSTRAINT "user_stats_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_admin_id_users_id_fk" FOREIGN KEY ("admin_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_question_log" ADD CONSTRAINT "user_question_log_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_question_log" ADD CONSTRAINT "user_question_log_question_bank_id_question_bank_id_fk" FOREIGN KEY ("question_bank_id") REFERENCES "public"."question_bank"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "email_verification_codes" ADD CONSTRAINT "email_verification_codes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "password_reset_tokens" ADD CONSTRAINT "password_reset_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/db/migrations/meta/0000_snapshot.json b/src/db/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..ca0b543 --- /dev/null +++ b/src/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,1231 @@ +{ + "id": "2a8f572c-86d8-47f3-af8a-cb158e81d93e", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "nickname": { + "name": "nickname", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "self_level": { + "name": "self_level", + "type": "self_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "email_verified_at": { + "name": "email_verified_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "refresh_token_hash": { + "name": "refresh_token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_agent": { + "name": "user_agent", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "plan", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_provider": { + "name": "payment_provider", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "subscriptions_user_id_users_id_fk": { + "name": "subscriptions_user_id_users_id_fk", + "tableFrom": "subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "subscriptions_user_id_unique": { + "name": "subscriptions_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tests": { + "name": "tests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stack": { + "name": "stack", + "type": "stack", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "test_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "status": { + "name": "status", + "type": "test_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'in_progress'" + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "time_limit_seconds": { + "name": "time_limit_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "tests_user_id_users_id_fk": { + "name": "tests_user_id_users_id_fk", + "tableFrom": "tests", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.test_questions": { + "name": "test_questions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "test_id": { + "name": "test_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_bank_id": { + "name": "question_bank_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "order_number": { + "name": "order_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "question_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "question_text": { + "name": "question_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "correct_answer": { + "name": "correct_answer", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_answer": { + "name": "user_answer", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_correct": { + "name": "is_correct", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "answered_at": { + "name": "answered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "test_questions_test_id_tests_id_fk": { + "name": "test_questions_test_id_tests_id_fk", + "tableFrom": "test_questions", + "tableTo": "tests", + "columnsFrom": [ + "test_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "test_questions_question_bank_id_question_bank_id_fk": { + "name": "test_questions_question_bank_id_question_bank_id_fk", + "tableFrom": "test_questions", + "tableTo": "question_bank", + "columnsFrom": [ + "question_bank_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.question_bank": { + "name": "question_bank", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "stack": { + "name": "stack", + "type": "stack", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "question_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "question_text": { + "name": "question_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "correct_answer": { + "name": "correct_answer", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "question_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "source": { + "name": "source", + "type": "question_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "usage_count": { + "name": "usage_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()" + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.question_cache_meta": { + "name": "question_cache_meta", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "question_bank_id": { + "name": "question_bank_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "llm_model": { + "name": "llm_model", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "prompt_hash": { + "name": "prompt_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "generation_time_ms": { + "name": "generation_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "raw_response": { + "name": "raw_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "question_cache_meta_question_bank_id_question_bank_id_fk": { + "name": "question_cache_meta_question_bank_id_question_bank_id_fk", + "tableFrom": "question_cache_meta", + "tableTo": "question_bank", + "columnsFrom": [ + "question_bank_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.question_reports": { + "name": "question_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "question_bank_id": { + "name": "question_bank_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "report_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "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 + } + }, + "indexes": {}, + "foreignKeys": { + "question_reports_question_bank_id_question_bank_id_fk": { + "name": "question_reports_question_bank_id_question_bank_id_fk", + "tableFrom": "question_reports", + "tableTo": "question_bank", + "columnsFrom": [ + "question_bank_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "question_reports_user_id_users_id_fk": { + "name": "question_reports_user_id_users_id_fk", + "tableFrom": "question_reports", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stack": { + "name": "stack", + "type": "stack", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "total_questions": { + "name": "total_questions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "correct_answers": { + "name": "correct_answers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tests_taken": { + "name": "tests_taken", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_test_at": { + "name": "last_test_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_users_id_fk": { + "name": "user_stats_user_id_users_id_fk", + "tableFrom": "user_stats", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_stack_level_unique": { + "name": "user_stats_user_id_stack_level_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "stack", + "level" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "admin_id": { + "name": "admin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_admin_id_users_id_fk": { + "name": "audit_logs_admin_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_question_log": { + "name": "user_question_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_bank_id": { + "name": "question_bank_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_question_log_user_id_users_id_fk": { + "name": "user_question_log_user_id_users_id_fk", + "tableFrom": "user_question_log", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_question_log_question_bank_id_question_bank_id_fk": { + "name": "user_question_log_question_bank_id_question_bank_id_fk", + "tableFrom": "user_question_log", + "tableTo": "question_bank", + "columnsFrom": [ + "question_bank_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_verification_codes": { + "name": "email_verification_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(10)", + "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": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_verification_codes_user_id_users_id_fk": { + "name": "email_verification_codes_user_id_users_id_fk", + "tableFrom": "email_verification_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_tokens": { + "name": "password_reset_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "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": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.level": { + "name": "level", + "schema": "public", + "values": [ + "basic", + "beginner", + "intermediate", + "advanced", + "expert" + ] + }, + "public.plan": { + "name": "plan", + "schema": "public", + "values": [ + "free", + "pro" + ] + }, + "public.question_source": { + "name": "question_source", + "schema": "public", + "values": [ + "llm_generated", + "manual" + ] + }, + "public.question_status": { + "name": "question_status", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected" + ] + }, + "public.question_type": { + "name": "question_type", + "schema": "public", + "values": [ + "single_choice", + "multiple_select", + "true_false", + "short_text" + ] + }, + "public.report_status": { + "name": "report_status", + "schema": "public", + "values": [ + "open", + "resolved", + "dismissed" + ] + }, + "public.self_level": { + "name": "self_level", + "schema": "public", + "values": [ + "jun", + "mid", + "sen" + ] + }, + "public.stack": { + "name": "stack", + "schema": "public", + "values": [ + "html", + "css", + "js", + "ts", + "react", + "vue", + "nodejs", + "git", + "web_basics" + ] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "active", + "trialing", + "cancelled", + "expired" + ] + }, + "public.test_mode": { + "name": "test_mode", + "schema": "public", + "values": [ + "fixed", + "infinite", + "marathon" + ] + }, + "public.test_status": { + "name": "test_status", + "schema": "public", + "values": [ + "in_progress", + "completed", + "abandoned" + ] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "guest", + "free", + "pro", + "admin" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json new file mode 100644 index 0000000..cc4ebdc --- /dev/null +++ b/src/db/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1772620981431, + "tag": "0000_fearless_salo", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/db/seed.ts b/src/db/seed.ts new file mode 100644 index 0000000..c0b674d --- /dev/null +++ b/src/db/seed.ts @@ -0,0 +1,47 @@ +import 'dotenv/config'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import { eq } from 'drizzle-orm'; +import pg from 'pg'; +import argon2 from 'argon2'; +import { env } from '../config/env.js'; +import { users } from './schema/index.js'; + +const { Pool } = pg; + +const TEST_USER = { + email: 'test@example.com', + password: 'TestPassword123!', + nickname: 'TestUser', +}; + +async function runSeed() { + const pool = new Pool({ connectionString: env.DATABASE_URL }); + const db = drizzle(pool); + + if (env.NODE_ENV !== 'development') { + await pool.end(); + return; + } + + const existing = await db.select().from(users).where(eq(users.email, TEST_USER.email)).limit(1); + if (existing.length > 0) { + await pool.end(); + return; + } + + const passwordHash = await argon2.hash(TEST_USER.password); + await db.insert(users).values({ + email: TEST_USER.email, + passwordHash, + nickname: TEST_USER.nickname, + role: 'free', + emailVerifiedAt: new Date(), + }); + + await pool.end(); +} + +runSeed().catch((err) => { + console.error('Seed failed:', err); + process.exit(1); +});