2026-03-04 18:07:18 +01:00
|
|
|
/**
|
|
|
|
|
* 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(
|
2026-03-04 19:33:33 +01:00
|
|
|
`SELECT id, title, description, filename_disk, type, filesize, duration, uploaded_by, uploaded_on as date_created
|
2026-03-04 18:07:18 +01:00
|
|
|
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,
|
2026-03-04 19:33:33 +01:00
|
|
|
u.description, u.avatar, u.join_date as date_created,
|
|
|
|
|
u.artist_name, u.slug,
|
2026-03-04 18:07:18 +01:00
|
|
|
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,
|
2026-03-04 19:33:33 +01:00
|
|
|
true,
|
2026-03-04 18:07:18 +01:00
|
|
|
user.date_created,
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
migrated++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(` ✅ Users: ${migrated} migrated`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function migrateUserPhotos() {
|
|
|
|
|
console.log("🖼️ Migrating user photos...");
|
|
|
|
|
const { rows } = await query(
|
2026-03-04 19:33:33 +01:00
|
|
|
`SELECT directus_users_id as user_id, directus_files_id as file_id
|
2026-03-04 18:07:18 +01:00
|
|
|
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`,
|
2026-03-04 19:33:33 +01:00
|
|
|
[row.user_id, row.file_id, 0],
|
2026-03-04 18:07:18 +01:00
|
|
|
);
|
|
|
|
|
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,
|
2026-03-04 19:33:33 +01:00
|
|
|
premium, featured
|
2026-03-04 18:07:18 +01:00
|
|
|
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,
|
2026-03-04 19:33:33 +01:00
|
|
|
0,
|
|
|
|
|
0,
|
2026-03-04 18:07:18 +01:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
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,
|
2026-03-04 19:33:33 +01:00
|
|
|
user_created as user_id, status, tags, linked_video, public,
|
2026-03-04 18:07:18 +01:00
|
|
|
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,
|
2026-03-04 19:33:33 +01:00
|
|
|
user_id, status, tags, linked_video, public,
|
2026-03-04 18:07:18 +01:00
|
|
|
original_recording_id, date_created, date_updated)
|
2026-03-04 19:33:33 +01:00
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
2026-03-04 18:07:18 +01:00
|
|
|
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();
|