chore: add initial migration and seed script

Made-with: Cursor
This commit is contained in:
Anton
2026-03-04 13:57:10 +03:00
parent c7c9982234
commit 41b4f48a0f
6 changed files with 1539 additions and 2 deletions

View File

@@ -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: {

View File

@@ -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",

View File

@@ -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 $$;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1772620981431,
"tag": "0000_fearless_salo",
"breakpoints": true
}
]
}

47
src/db/seed.ts Normal file
View File

@@ -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);
});