From 012a36e11b6bd2943e9580da81edfd2470e84ab4 Mon Sep 17 00:00:00 2001 From: vidane Date: Sat, 2 May 2026 18:03:28 -0400 Subject: [PATCH] Add src/server/db/migrate.ts --- src/server/db/migrate.ts | 131 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/server/db/migrate.ts diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts new file mode 100644 index 0000000..442e153 --- /dev/null +++ b/src/server/db/migrate.ts @@ -0,0 +1,131 @@ +import { Pool } from 'pg'; + +const connectionString = process.env.DATABASE_URL || 'postgresql://vixtix:vixtix_secret@localhost:5433/vixtix'; +const pool = new Pool({ connectionString }); + +async function migrate() { + console.log('Running migrations...'); + + const client = await pool.connect(); + try { + // Create extension for UUID + await client.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); + + // Create enums + await client.query(` + DO \$\$ BEGIN + CREATE TYPE priority AS ENUM ('low', 'medium', 'high', 'urgent'); + EXCEPTION + WHEN duplicate_type THEN NOTHING; + END \$\$; + `); + + await client.query(` + DO \$\$ BEGIN + CREATE TYPE status AS ENUM ('todo', 'in_progress', 'done'); + EXCEPTION + WHEN duplicate_type THEN NOTHING; + END \$\$; + `); + + await client.query(` + DO \$\$ BEGIN + CREATE TYPE recurrence AS ENUM ('daily', 'weekly', 'biweekly', 'monthly', 'yearly', 'none'); + EXCEPTION + WHEN duplicate_type THEN NOTHING; + END \$\$; + `); + + // Create tables + await client.query(` + CREATE TABLE IF NOT EXISTS projects ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + description TEXT DEFAULT '', + color TEXT DEFAULT '#3b82f6', + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL + ); + `); + + await client.query(` + CREATE TABLE IF NOT EXISTS tasks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID REFERENCES projects(id), + title TEXT NOT NULL, + description TEXT DEFAULT '', + completed BOOLEAN DEFAULT FALSE NOT NULL, + priority priority DEFAULT 'medium' NOT NULL, + due_date TIMESTAMP WITH TIME ZONE, + status status DEFAULT 'todo' NOT NULL, + parent_task_id UUID REFERENCES tasks(id), + recurrence_rule recurrence DEFAULT 'none' NOT NULL, + recurrence_interval INTEGER DEFAULT 1 NOT NULL, + next_occurrence TIMESTAMP WITH TIME ZONE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL + ); + `); + + // Create indexes + await client.query(` + CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id); + CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id ON tasks(parent_task_id); + CREATE INDEX IF NOT EXISTS idx_tasks_completed ON tasks(completed); + CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); + CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date); + CREATE INDEX IF NOT EXISTS idx_tasks_recurrence ON tasks(recurrence_rule); + `); + + // Seed data + const projects = await client.query('SELECT COUNT(*) FROM projects'); + if (parseInt(projects.rows[0].count) === 0) { + console.log('Seeding initial data...'); + await client.query(` + INSERT INTO projects (name, description, color, sort_order) VALUES + ('Personal', 'Personal tasks and goals', '#3b82f6', 1), + ('Work', 'Work-related tasks', '#10b981', 2), + ('Health', 'Health and fitness', '#f59e0b', 3), + ('Finance', 'Financial tasks and tracking', '#8b5cf6', 4); + `); + + // Seed some sample tasks + await client.query(` + INSERT INTO tasks (project_id, title, description, priority, status, due_date, sort_order) + SELECT p.id, t.title, t.description, t.priority, t.status, t.due_date, t.sort_order + FROM ( + VALUES + ('Personal', 'Set up daily routine', 'Morning meditation, exercise, and planning', 'high', 'todo', NOW() + INTERVAL '1 day', 1), + ('Personal', 'Read 30 minutes', 'Read a book or articles', 'medium', 'todo', NOW() + INTERVAL '2 days', 2), + ('Personal', 'Clean apartment', 'Deep clean kitchen and bathrooms', 'medium', 'in_progress', NOW() + INTERVAL '3 days', 3), + ('Work', 'Review sprint backlog', 'Prioritize tasks for next sprint', 'high', 'todo', NOW() + INTERVAL '1 day', 1), + ('Work', 'Update documentation', 'Add API docs for new endpoints', 'medium', 'in_progress', NOW() + INTERVAL '5 days', 2), + ('Work', 'Code review', 'Review pull requests from team', 'low', 'done', NOW() - INTERVAL '1 day', 3), + ('Health', 'Gym workout', 'Upper body strength training', 'high', 'todo', NOW() + INTERVAL '1 day', 1), + ('Health', 'Meal prep', 'Prepare healthy meals for the week', 'medium', 'todo', NOW() + INTERVAL '2 days', 2), + ('Health', 'Track water intake', 'Drink at least 8 glasses of water', 'low', 'done', NOW() - INTERVAL '1 day', 3), + ('Finance', 'Review monthly budget', 'Check spending and adjust categories', 'high', 'todo', NOW() + INTERVAL '3 days', 1), + ('Finance', 'Pay bills', 'Electricity, internet, phone', 'urgent', 'todo', NOW() + INTERVAL '1 day', 2), + ('Finance', 'Investment review', 'Check portfolio performance', 'medium', 'todo', NOW() + INTERVAL '7 days', 3) + ) AS t(name, title, description, priority, status, due_date, sort_order) + JOIN projects p ON p.name = t.name; + `); + + console.log('Seed data inserted successfully!'); + } else { + console.log('Database already seeded.'); + } + + console.log('Migrations completed successfully!'); + } catch (error) { + console.error('Migration failed:', error); + process.exit(1); + } finally { + client.release(); + await pool.end(); + } +} + +migrate();