/** * Data Migration: Directus → Custom Backend * * Migrates data from Directus tables to the new schema. * Run with: tsx src/scripts/data-migration.ts * * Environment variables: * DATABASE_URL - PostgreSQL connection (same DB) * OLD_UPLOAD_DIR - Path to Directus uploads (e.g. /old-uploads) * NEW_UPLOAD_DIR - Path to new upload dir (e.g. /data/uploads) */ import { Pool } from "pg"; import fs from "fs"; import path from "path"; const DATABASE_URL = process.env.DATABASE_URL || "postgresql://sexy:sexy@localhost:5432/sexy"; const OLD_UPLOAD_DIR = process.env.OLD_UPLOAD_DIR || "/old-uploads"; const NEW_UPLOAD_DIR = process.env.NEW_UPLOAD_DIR || "/data/uploads"; const pool = new Pool({ connectionString: DATABASE_URL }); async function query(sql: string, params: unknown[] = []) { const client = await pool.connect(); try { return await client.query(sql, params); } finally { client.release(); } } function copyFile(src: string, dest: string) { const dir = path.dirname(dest); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } if (fs.existsSync(src)) { fs.copyFileSync(src, dest); return true; } return false; } async function migrateFiles() { console.log("šŸ“ Migrating files..."); const { rows } = await query( `SELECT id, title, description, filename_disk, type, filesize, duration, uploaded_by, uploaded_on as date_created FROM directus_files`, ); let migrated = 0; let skipped = 0; for (const file of rows) { // Check if already migrated const existing = await query("SELECT id FROM files WHERE id = $1", [file.id]); if (existing.rows.length > 0) { skipped++; continue; } await query( `INSERT INTO files (id, title, description, filename, mime_type, filesize, duration, uploaded_by, date_created) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (id) DO NOTHING`, [ file.id, file.title, file.description, file.filename_disk || `${file.id}`, file.type, file.filesize, file.duration, file.uploaded_by, file.date_created, ], ); // Copy file to new location const srcPath = path.join(OLD_UPLOAD_DIR, file.filename_disk || ""); const destPath = path.join(NEW_UPLOAD_DIR, file.id, file.filename_disk || `${file.id}`); const copied = copyFile(srcPath, destPath); if (!copied) { console.warn(` āš ļø File not found on disk: ${file.filename_disk}`); } migrated++; } console.log(` āœ… Files: ${migrated} migrated, ${skipped} already existed`); } async function migrateUsers() { console.log("šŸ‘„ Migrating users..."); const { rows } = await query( `SELECT u.id, u.email, u.password, u.first_name, u.last_name, u.description, u.avatar, u.join_date as date_created, u.artist_name, u.slug, r.name as role_name FROM directus_users u LEFT JOIN directus_roles r ON u.role = r.id WHERE u.status = 'active'`, ); let migrated = 0; for (const user of rows) { const existing = await query("SELECT id FROM users WHERE id = $1", [user.id]); if (existing.rows.length > 0) { migrated++; continue; } const role = user.role_name === "Model" ? "model" : user.role_name === "Administrator" ? "admin" : "viewer"; // Fetch tags from custom user fields if they exist let tags: string[] = []; try { const tagsRes = await query("SELECT tags FROM directus_users WHERE id = $1", [user.id]); if (tagsRes.rows[0]?.tags) { tags = Array.isArray(tagsRes.rows[0].tags) ? tagsRes.rows[0].tags : JSON.parse(tagsRes.rows[0].tags || "[]"); } } catch {} await query( `INSERT INTO users (id, email, password_hash, first_name, last_name, artist_name, slug, description, tags, role, avatar, email_verified, date_created) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) ON CONFLICT (id) DO NOTHING`, [ user.id, user.email, user.password || "MIGRATED_NO_PASSWORD", user.first_name, user.last_name, user.artist_name, user.slug, user.description, JSON.stringify(tags), role, user.avatar, true, user.date_created, ], ); migrated++; } console.log(` āœ… Users: ${migrated} migrated`); } async function migrateUserPhotos() { console.log("šŸ–¼ļø Migrating user photos..."); const { rows } = await query( `SELECT directus_users_id as user_id, directus_files_id as file_id FROM junction_directus_users_files`, ); let migrated = 0; for (const row of rows) { const userExists = await query("SELECT id FROM users WHERE id = $1", [row.user_id]); const fileExists = await query("SELECT id FROM files WHERE id = $1", [row.file_id]); if (!userExists.rows.length || !fileExists.rows.length) continue; await query( `INSERT INTO user_photos (user_id, file_id, sort) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`, [row.user_id, row.file_id, 0], ); migrated++; } console.log(` āœ… User photos: ${migrated} migrated`); } async function migrateArticles() { console.log("šŸ“° Migrating articles..."); const { rows } = await query( `SELECT id, slug, title, excerpt, content, image, tags, publish_date, author, category, featured, date_created, date_updated FROM sexy_articles`, ); let migrated = 0; for (const article of rows) { await query( `INSERT INTO articles (id, slug, title, excerpt, content, image, tags, publish_date, author, category, featured, date_created, date_updated) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) ON CONFLICT (id) DO NOTHING`, [ article.id, article.slug, article.title, article.excerpt, article.content, article.image, Array.isArray(article.tags) ? JSON.stringify(article.tags) : article.tags, article.publish_date, article.author, article.category, article.featured, article.date_created, article.date_updated, ], ); migrated++; } console.log(` āœ… Articles: ${migrated} migrated`); } async function migrateVideos() { console.log("šŸŽ¬ Migrating videos..."); const { rows } = await query( `SELECT id, slug, title, description, image, movie, tags, upload_date, premium, featured FROM sexy_videos`, ); let migrated = 0; for (const video of rows) { await query( `INSERT INTO videos (id, slug, title, description, image, movie, tags, upload_date, premium, featured, likes_count, plays_count) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (id) DO NOTHING`, [ video.id, video.slug, video.title, video.description, video.image, video.movie, Array.isArray(video.tags) ? JSON.stringify(video.tags) : video.tags, video.upload_date, video.premium, video.featured, 0, 0, ], ); migrated++; } console.log(` āœ… Videos: ${migrated} migrated`); } async function migrateVideoModels() { console.log("šŸ”— Migrating video models..."); const { rows } = await query( `SELECT sexy_videos_id as video_id, directus_users_id as user_id FROM sexy_videos_models`, ); let migrated = 0; for (const row of rows) { const videoExists = await query("SELECT id FROM videos WHERE id = $1", [row.video_id]); const userExists = await query("SELECT id FROM users WHERE id = $1", [row.user_id]); if (!videoExists.rows.length || !userExists.rows.length) continue; await query( `INSERT INTO video_models (video_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [row.video_id, row.user_id], ); migrated++; } console.log(` āœ… Video models: ${migrated} migrated`); } async function migrateVideoLikes() { console.log("ā¤ļø Migrating video likes..."); const { rows } = await query( `SELECT id, video_id, user_id, date_created FROM sexy_video_likes`, ); let migrated = 0; for (const row of rows) { await query( `INSERT INTO video_likes (id, video_id, user_id, date_created) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO NOTHING`, [row.id, row.video_id, row.user_id, row.date_created], ); migrated++; } console.log(` āœ… Video likes: ${migrated} migrated`); } async function migrateVideoPlays() { console.log("ā–¶ļø Migrating video plays..."); const { rows } = await query( `SELECT id, video_id, user_id, session_id, duration_watched, completed, date_created FROM sexy_video_plays`, ); let migrated = 0; for (const row of rows) { await query( `INSERT INTO video_plays (id, video_id, user_id, session_id, duration_watched, completed, date_created) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO NOTHING`, [ row.id, row.video_id, row.user_id, row.session_id, row.duration_watched, row.completed, row.date_created, ], ); migrated++; } console.log(` āœ… Video plays: ${migrated} migrated`); } async function migrateRecordings() { console.log("šŸŽ™ļø Migrating recordings..."); const { rows } = await query( `SELECT id, title, description, slug, duration, events, device_info, user_created as user_id, status, tags, linked_video, public, original_recording_id, date_created, date_updated FROM sexy_recordings`, ); let migrated = 0; for (const recording of rows) { await query( `INSERT INTO recordings (id, title, description, slug, duration, events, device_info, user_id, status, tags, linked_video, public, original_recording_id, date_created, date_updated) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) ON CONFLICT (id) DO NOTHING`, [ recording.id, recording.title, recording.description, recording.slug, recording.duration, typeof recording.events === "string" ? recording.events : JSON.stringify(recording.events), typeof recording.device_info === "string" ? recording.device_info : JSON.stringify(recording.device_info), recording.user_id, recording.status, Array.isArray(recording.tags) ? JSON.stringify(recording.tags) : recording.tags, recording.linked_video, recording.public, recording.original_recording_id, recording.date_created, recording.date_updated, ], ); migrated++; } console.log(` āœ… Recordings: ${migrated} migrated`); } async function migrateRecordingPlays() { console.log("ā–¶ļø Migrating recording plays..."); const { rows } = await query( `SELECT id, user_id, recording_id, duration_played, completed, date_created FROM sexy_recording_plays`, ); let migrated = 0; for (const row of rows) { await query( `INSERT INTO recording_plays (id, recording_id, user_id, duration_played, completed, date_created) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO NOTHING`, [row.id, row.recording_id, row.user_id, row.duration_played, row.completed, row.date_created], ); migrated++; } console.log(` āœ… Recording plays: ${migrated} migrated`); } async function migrateComments() { console.log("šŸ’¬ Migrating comments..."); const { rows } = await query( `SELECT id, collection, item, comment, user_created as user_id, date_created FROM directus_comments WHERE collection IN ('sexy_videos', 'sexy_recordings')`, ); let migrated = 0; for (const row of rows) { // Map collection names const collection = row.collection === "sexy_videos" ? "videos" : "recordings"; await query( `INSERT INTO comments (collection, item_id, comment, user_id, date_created) VALUES ($1, $2, $3, $4, $5)`, [collection, row.item, row.comment, row.user_id, row.date_created], ); migrated++; } console.log(` āœ… Comments: ${migrated} migrated`); } async function migrateAchievements() { console.log("šŸ† Migrating achievements..."); const { rows } = await query( `SELECT id, code, name, description, icon, category, required_count, points_reward, status, sort FROM sexy_achievements`, ); let migrated = 0; for (const row of rows) { await query( `INSERT INTO achievements (id, code, name, description, icon, category, required_count, points_reward, status, sort) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (id) DO NOTHING`, [ row.id, row.code, row.name, row.description, row.icon, row.category, row.required_count, row.points_reward, row.status, row.sort, ], ); migrated++; } console.log(` āœ… Achievements: ${migrated} migrated`); } async function migrateUserAchievements() { console.log("šŸŽ–ļø Migrating user achievements..."); const { rows } = await query( `SELECT user_id, achievement_id, progress, date_unlocked FROM sexy_user_achievements`, ); let migrated = 0; for (const row of rows) { const userExists = await query("SELECT id FROM users WHERE id = $1", [row.user_id]); const achievementExists = await query("SELECT id FROM achievements WHERE id = $1", [ row.achievement_id, ]); if (!userExists.rows.length || !achievementExists.rows.length) continue; await query( `INSERT INTO user_achievements (user_id, achievement_id, progress, date_unlocked) VALUES ($1, $2, $3, $4) ON CONFLICT (user_id, achievement_id) DO NOTHING`, [row.user_id, row.achievement_id, row.progress, row.date_unlocked], ); migrated++; } console.log(` āœ… User achievements: ${migrated} migrated`); } async function migrateUserPoints() { console.log("šŸ’Ž Migrating user points..."); const { rows } = await query( `SELECT user_id, action, points, recording_id, date_created FROM sexy_user_points`, ); let migrated = 0; for (const row of rows) { const userExists = await query("SELECT id FROM users WHERE id = $1", [row.user_id]); if (!userExists.rows.length) continue; await query( `INSERT INTO user_points (user_id, action, points, recording_id, date_created) VALUES ($1, $2, $3, $4, $5)`, [row.user_id, row.action, row.points, row.recording_id, row.date_created], ); migrated++; } console.log(` āœ… User points: ${migrated} migrated`); } async function migrateUserStats() { console.log("šŸ“Š Migrating user stats..."); const { rows } = await query( `SELECT user_id, total_raw_points, total_weighted_points, recordings_count, playbacks_count, comments_count, achievements_count, last_updated FROM sexy_user_stats`, ); let migrated = 0; for (const row of rows) { const userExists = await query("SELECT id FROM users WHERE id = $1", [row.user_id]); if (!userExists.rows.length) continue; await query( `INSERT INTO user_stats (user_id, total_raw_points, total_weighted_points, recordings_count, playbacks_count, comments_count, achievements_count, last_updated) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (user_id) DO NOTHING`, [ row.user_id, row.total_raw_points, row.total_weighted_points, row.recordings_count, row.playbacks_count, row.comments_count, row.achievements_count, row.last_updated, ], ); migrated++; } console.log(` āœ… User stats: ${migrated} migrated`); } async function main() { console.log("šŸš€ Starting data migration from Directus to custom backend...\n"); try { // Verify connection await query("SELECT 1"); console.log("āœ… Database connected\n"); // Migration order respects FK dependencies await migrateFiles(); await migrateUsers(); await migrateUserPhotos(); await migrateArticles(); await migrateVideos(); await migrateVideoModels(); await migrateVideoLikes(); await migrateVideoPlays(); await migrateRecordings(); await migrateRecordingPlays(); await migrateComments(); await migrateAchievements(); await migrateUserAchievements(); await migrateUserPoints(); await migrateUserStats(); console.log("\nšŸŽ‰ Migration complete!"); } catch (error) { console.error("āŒ Migration failed:", error); process.exit(1); } finally { await pool.end(); } } main();