migrate: replace pg with Prisma ORM
- Add prisma/schema.prisma with Project/Task models, enums, and relations - Create src/lib/db.ts singleton Prisma client - Refactor all 5 API routes to use Prisma queries - Replace migrate.ts with seed.ts for initial data - Update Dockerfile for Prisma lifecycle (copy generated client) - Update tsconfig.json with @/generated/* path alias - Remove pg and @types/pg dependencies - Add prisma.config.ts for Prisma 6 config - Update .gitignore for generated Prisma client
This commit is contained in:
@@ -1,124 +0,0 @@
|
||||
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 {
|
||||
await client.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
|
||||
|
||||
// Create enums - use DO block to handle duplicates
|
||||
await client.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'priority') THEN
|
||||
CREATE TYPE priority AS ENUM ('low', 'medium', 'high', 'urgent');
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
await client.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'status') THEN
|
||||
CREATE TYPE status AS ENUM ('todo', 'in_progress', 'done');
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
await client.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'recurrence') THEN
|
||||
CREATE TYPE recurrence AS ENUM ('daily', 'weekly', 'biweekly', 'monthly', 'yearly', 'none');
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
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 DATE,
|
||||
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 DATE,
|
||||
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 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);
|
||||
`);
|
||||
|
||||
const count = await client.query('SELECT COUNT(*) FROM projects');
|
||||
if (parseInt(count.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);
|
||||
`);
|
||||
|
||||
// Use DATE arithmetic instead of INTERVAL
|
||||
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::priority, t.status::status, t.due_date, t.sort_order
|
||||
FROM (
|
||||
VALUES
|
||||
('Personal', 'Set up daily routine', 'Morning meditation, exercise, and planning', 'high', 'todo', CURRENT_DATE + 1, 1),
|
||||
('Personal', 'Read 30 minutes', 'Read a book or articles', 'medium', 'todo', CURRENT_DATE + 2, 2),
|
||||
('Personal', 'Clean apartment', 'Deep clean kitchen and bathrooms', 'medium', 'in_progress', CURRENT_DATE + 3, 3),
|
||||
('Work', 'Review sprint backlog', 'Prioritize tasks for next sprint', 'high', 'todo', CURRENT_DATE + 1, 1),
|
||||
('Work', 'Update documentation', 'Add API docs for new endpoints', 'medium', 'in_progress', CURRENT_DATE + 5, 2),
|
||||
('Work', 'Code review', 'Review pull requests from team', 'low', 'done', CURRENT_DATE - 1, 3),
|
||||
('Health', 'Gym workout', 'Upper body strength training', 'high', 'todo', CURRENT_DATE + 1, 1),
|
||||
('Health', 'Meal prep', 'Prepare healthy meals for the week', 'medium', 'todo', CURRENT_DATE + 2, 2),
|
||||
('Health', 'Track water intake', 'Drink at least 8 glasses of water', 'low', 'done', CURRENT_DATE - 1, 3),
|
||||
('Finance', 'Review monthly budget', 'Check spending and adjust categories', 'high', 'todo', CURRENT_DATE + 3, 1),
|
||||
('Finance', 'Pay bills', 'Electricity, internet, phone', 'urgent', 'todo', CURRENT_DATE + 1, 2),
|
||||
('Finance', 'Investment review', 'Check portfolio performance', 'medium', 'todo', CURRENT_DATE + 7, 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();
|
||||
63
src/server/db/seed.ts
Normal file
63
src/server/db/seed.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
});
|
||||
|
||||
async function seed() {
|
||||
console.log('Seeding database...');
|
||||
|
||||
const count = await prisma.project.count();
|
||||
if (count > 0) {
|
||||
console.log('Database already seeded.');
|
||||
return;
|
||||
}
|
||||
|
||||
const projects = await prisma.project.createManyAndReturn({
|
||||
data: [
|
||||
{ name: 'Personal', description: 'Personal tasks and goals', color: '#3b82f6', sortOrder: 1 },
|
||||
{ name: 'Work', description: 'Work-related tasks', color: '#10b981', sortOrder: 2 },
|
||||
{ name: 'Health', description: 'Health and fitness', color: '#f59e0b', sortOrder: 3 },
|
||||
{ name: 'Finance', description: 'Financial tasks and tracking', color: '#8b5cf6', sortOrder: 4 },
|
||||
],
|
||||
}) as Array<{ id: string; name: string }>;
|
||||
|
||||
// Map project names to IDs
|
||||
const projectMap = new Map(projects.map(p => [p.name, p.id]));
|
||||
|
||||
const today = new Date();
|
||||
const tasks: Array<{
|
||||
projectId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
priority: 'low' | 'medium' | 'high' | 'urgent';
|
||||
status: 'todo' | 'in_progress' | 'done';
|
||||
dueDate: Date;
|
||||
sortOrder: number;
|
||||
}> = [
|
||||
{ projectId: projectMap.get('Personal')!, title: 'Set up daily routine', description: 'Morning meditation, exercise, and planning', priority: 'high', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1), sortOrder: 1 },
|
||||
{ projectId: projectMap.get('Personal')!, title: 'Read 30 minutes', description: 'Read a book or articles', priority: 'medium', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2), sortOrder: 2 },
|
||||
{ projectId: projectMap.get('Personal')!, title: 'Clean apartment', description: 'Deep clean kitchen and bathrooms', priority: 'medium', status: 'in_progress', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3), sortOrder: 3 },
|
||||
{ projectId: projectMap.get('Work')!, title: 'Review sprint backlog', description: 'Prioritize tasks for next sprint', priority: 'high', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1), sortOrder: 1 },
|
||||
{ projectId: projectMap.get('Work')!, title: 'Update documentation', description: 'Add API docs for new endpoints', priority: 'medium', status: 'in_progress', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 5), sortOrder: 2 },
|
||||
{ projectId: projectMap.get('Work')!, title: 'Code review', description: 'Review pull requests from team', priority: 'low', status: 'done', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1), sortOrder: 3 },
|
||||
{ projectId: projectMap.get('Health')!, title: 'Gym workout', description: 'Upper body strength training', priority: 'high', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1), sortOrder: 1 },
|
||||
{ projectId: projectMap.get('Health')!, title: 'Meal prep', description: 'Prepare healthy meals for the week', priority: 'medium', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2), sortOrder: 2 },
|
||||
{ projectId: projectMap.get('Health')!, title: 'Track water intake', description: 'Drink at least 8 glasses of water', priority: 'low', status: 'done', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1), sortOrder: 3 },
|
||||
{ projectId: projectMap.get('Finance')!, title: 'Review monthly budget', description: 'Check spending and adjust categories', priority: 'high', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3), sortOrder: 1 },
|
||||
{ projectId: projectMap.get('Finance')!, title: 'Pay bills', description: 'Electricity, internet, phone', priority: 'urgent', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1), sortOrder: 2 },
|
||||
{ projectId: projectMap.get('Finance')!, title: 'Investment review', description: 'Check portfolio performance', priority: 'medium', status: 'todo', dueDate: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7), sortOrder: 3 },
|
||||
];
|
||||
|
||||
await prisma.task.createMany({ data: tasks });
|
||||
console.log('Seed data inserted successfully!');
|
||||
}
|
||||
|
||||
seed()
|
||||
.catch((e) => {
|
||||
console.error('Seed failed:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user