chore: add initial migration and seed script
Made-with: Cursor
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
246
src/db/migrations/0000_fearless_salo.sql
Normal file
246
src/db/migrations/0000_fearless_salo.sql
Normal 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 $$;
|
||||
1231
src/db/migrations/meta/0000_snapshot.json
Normal file
1231
src/db/migrations/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
13
src/db/migrations/meta/_journal.json
Normal file
13
src/db/migrations/meta/_journal.json
Normal 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
47
src/db/seed.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user