Spaces:
Running
Running
Commit ·
af9aabf
1
Parent(s): 8483276
fix: badges save/check, year selector for contributions
Browse files- scripts/check-mongo-data.ts +52 -0
- scripts/cleanup-mentorship-requests.ts +108 -0
- scripts/delete-null-requests.ts +47 -0
- scripts/migrate-data.ts +949 -0
- src/lib/github-contributions.ts +4 -0
scripts/check-mongo-data.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { MongoClient } from "mongodb";
|
| 2 |
+
import * as dotenv from "dotenv";
|
| 3 |
+
|
| 4 |
+
dotenv.config({ path: ".env.local" });
|
| 5 |
+
|
| 6 |
+
async function check() {
|
| 7 |
+
const mongoUri = process.env.MONGO_URL;
|
| 8 |
+
const mongoClient = new MongoClient(mongoUri!);
|
| 9 |
+
await mongoClient.connect();
|
| 10 |
+
const db = mongoClient.db(process.env.DB_NAME || "opentriage_db");
|
| 11 |
+
|
| 12 |
+
console.log("Checking MongoDB collections...\n");
|
| 13 |
+
|
| 14 |
+
// Check mentors
|
| 15 |
+
const mentors = await db.collection("mentors").find().toArray();
|
| 16 |
+
console.log(`Mentors: ${mentors.length}`);
|
| 17 |
+
if (mentors.length > 0) {
|
| 18 |
+
console.log("First mentor:", JSON.stringify(mentors[0], null, 2));
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// Check trophies/badges
|
| 22 |
+
const trophies = await db.collection("trophies").find().toArray();
|
| 23 |
+
console.log(`\nTrophies: ${trophies.length}`);
|
| 24 |
+
if (trophies.length > 0) {
|
| 25 |
+
console.log("First trophy:", JSON.stringify(trophies[0], null, 2));
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// Check badges collection
|
| 29 |
+
const badges = await db.collection("badges").find().toArray();
|
| 30 |
+
console.log(`\nBadges: ${badges.length}`);
|
| 31 |
+
if (badges.length > 0) {
|
| 32 |
+
console.log("First badge:", JSON.stringify(badges[0], null, 2));
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Check chat_history
|
| 36 |
+
const chatHistory = await db.collection("chat_history").find().limit(2).toArray();
|
| 37 |
+
console.log(`\nChat history: ${chatHistory.length} (showing first 2)`);
|
| 38 |
+
if (chatHistory.length > 0) {
|
| 39 |
+
console.log("First chat:", JSON.stringify(chatHistory[0], null, 2));
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Check messages
|
| 43 |
+
const messages = await db.collection("messages").find().limit(2).toArray();
|
| 44 |
+
console.log(`\nMessages: ${messages.length} (showing first 2)`);
|
| 45 |
+
if (messages.length > 0) {
|
| 46 |
+
console.log("First message:", JSON.stringify(messages[0], null, 2));
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
await mongoClient.close();
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
check().catch(console.error);
|
scripts/cleanup-mentorship-requests.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Cleanup Script: Remove duplicate mentorship requests
|
| 3 |
+
* Keeps only the latest request per mentor-mentee pair
|
| 4 |
+
*
|
| 5 |
+
* Run with: npx tsx scripts/cleanup-mentorship-requests.ts
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import * as dotenv from 'dotenv';
|
| 9 |
+
dotenv.config({ path: '.env.local' });
|
| 10 |
+
|
| 11 |
+
import { createClient } from "@libsql/client";
|
| 12 |
+
import { drizzle } from "drizzle-orm/libsql";
|
| 13 |
+
import { mentorshipRequests } from "../src/db/schema";
|
| 14 |
+
import { sql, desc, eq } from "drizzle-orm";
|
| 15 |
+
|
| 16 |
+
// Create client with env loaded
|
| 17 |
+
const client = createClient({
|
| 18 |
+
url: process.env.TURSO_DATABASE_URL!,
|
| 19 |
+
authToken: process.env.TURSO_AUTH_TOKEN,
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
const db = drizzle(client);
|
| 23 |
+
|
| 24 |
+
async function cleanupMentorshipRequests() {
|
| 25 |
+
console.log("🧹 Cleaning up mentorship requests...\n");
|
| 26 |
+
|
| 27 |
+
// Get all mentorship requests
|
| 28 |
+
const allRequests = await db.select()
|
| 29 |
+
.from(mentorshipRequests)
|
| 30 |
+
.orderBy(desc(mentorshipRequests.createdAt));
|
| 31 |
+
|
| 32 |
+
console.log(`Found ${allRequests.length} total requests:\n`);
|
| 33 |
+
|
| 34 |
+
allRequests.forEach(req => {
|
| 35 |
+
console.log(` ID: ${req.id}`);
|
| 36 |
+
console.log(` From: ${req.menteeUsername} -> To: ${req.mentorUsername}`);
|
| 37 |
+
console.log(` Status: ${req.status}`);
|
| 38 |
+
console.log(` Created: ${req.createdAt}`);
|
| 39 |
+
console.log(` Message: ${req.message?.substring(0, 50)}...`);
|
| 40 |
+
console.log("");
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
// Group by mentor-mentee pair
|
| 44 |
+
const pairs = new Map<string, typeof allRequests>();
|
| 45 |
+
|
| 46 |
+
allRequests.forEach(req => {
|
| 47 |
+
const key = `${req.menteeId}-${req.mentorId}`;
|
| 48 |
+
if (!pairs.has(key)) {
|
| 49 |
+
pairs.set(key, []);
|
| 50 |
+
}
|
| 51 |
+
pairs.get(key)!.push(req);
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
console.log(`\nFound ${pairs.size} unique mentor-mentee pairs`);
|
| 55 |
+
|
| 56 |
+
// Find duplicates to delete (keep only the most recent)
|
| 57 |
+
const toDelete: string[] = [];
|
| 58 |
+
|
| 59 |
+
pairs.forEach((requests, key) => {
|
| 60 |
+
if (requests.length > 1) {
|
| 61 |
+
// Sort by createdAt descending, keep first (most recent)
|
| 62 |
+
const sorted = requests.sort((a, b) =>
|
| 63 |
+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
| 64 |
+
);
|
| 65 |
+
|
| 66 |
+
console.log(`\n📋 Pair: ${sorted[0].menteeUsername} -> ${sorted[0].mentorUsername}`);
|
| 67 |
+
console.log(` Keeping: ${sorted[0].id} (${sorted[0].createdAt})`);
|
| 68 |
+
|
| 69 |
+
for (let i = 1; i < sorted.length; i++) {
|
| 70 |
+
console.log(` Deleting: ${sorted[i].id} (${sorted[i].createdAt})`);
|
| 71 |
+
toDelete.push(sorted[i].id);
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
if (toDelete.length === 0) {
|
| 77 |
+
console.log("\n✅ No duplicates found. Nothing to clean up.");
|
| 78 |
+
return;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
console.log(`\n🗑️ Will delete ${toDelete.length} duplicate requests.`);
|
| 82 |
+
console.log("Press Ctrl+C to cancel, or wait 3 seconds to continue...");
|
| 83 |
+
|
| 84 |
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
| 85 |
+
|
| 86 |
+
// Delete duplicates
|
| 87 |
+
for (const id of toDelete) {
|
| 88 |
+
await db.delete(mentorshipRequests)
|
| 89 |
+
.where(eq(mentorshipRequests.id, id));
|
| 90 |
+
console.log(` Deleted: ${id}`);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
console.log(`\n✅ Cleanup complete. Deleted ${toDelete.length} duplicates.`);
|
| 94 |
+
|
| 95 |
+
// Show remaining requests
|
| 96 |
+
const remaining = await db.select().from(mentorshipRequests);
|
| 97 |
+
console.log(`\n📊 Remaining requests: ${remaining.length}`);
|
| 98 |
+
remaining.forEach(req => {
|
| 99 |
+
console.log(` ${req.menteeUsername} -> ${req.mentorUsername} (${req.status})`);
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
cleanupMentorshipRequests()
|
| 104 |
+
.then(() => process.exit(0))
|
| 105 |
+
.catch(err => {
|
| 106 |
+
console.error("Error:", err);
|
| 107 |
+
process.exit(1);
|
| 108 |
+
});
|
scripts/delete-null-requests.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Quick cleanup: Delete requests with null usernames (test data)
|
| 3 |
+
*
|
| 4 |
+
* Run with: npx tsx scripts/delete-null-requests.ts
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import * as dotenv from 'dotenv';
|
| 8 |
+
dotenv.config({ path: '.env.local' });
|
| 9 |
+
|
| 10 |
+
import { createClient } from "@libsql/client";
|
| 11 |
+
import { drizzle } from "drizzle-orm/libsql";
|
| 12 |
+
import { mentorshipRequests } from "../src/db/schema";
|
| 13 |
+
import { isNull, or } from "drizzle-orm";
|
| 14 |
+
|
| 15 |
+
const client = createClient({
|
| 16 |
+
url: process.env.TURSO_DATABASE_URL!,
|
| 17 |
+
authToken: process.env.TURSO_AUTH_TOKEN,
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
const db = drizzle(client);
|
| 21 |
+
|
| 22 |
+
async function deleteNullRequests() {
|
| 23 |
+
console.log("🧹 Deleting requests with null usernames...\n");
|
| 24 |
+
|
| 25 |
+
// Delete requests where menteeUsername or mentorUsername is null
|
| 26 |
+
const result = await db.delete(mentorshipRequests)
|
| 27 |
+
.where(or(
|
| 28 |
+
isNull(mentorshipRequests.menteeUsername),
|
| 29 |
+
isNull(mentorshipRequests.mentorUsername)
|
| 30 |
+
));
|
| 31 |
+
|
| 32 |
+
console.log("✅ Deleted requests with null usernames");
|
| 33 |
+
|
| 34 |
+
// Show remaining
|
| 35 |
+
const remaining = await db.select().from(mentorshipRequests);
|
| 36 |
+
console.log(`\n📊 Remaining requests: ${remaining.length}`);
|
| 37 |
+
remaining.forEach(req => {
|
| 38 |
+
console.log(` ${req.menteeUsername} -> ${req.mentorUsername} (${req.status}) - ID: ${req.id}`);
|
| 39 |
+
});
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
deleteNullRequests()
|
| 43 |
+
.then(() => process.exit(0))
|
| 44 |
+
.catch(err => {
|
| 45 |
+
console.error("Error:", err);
|
| 46 |
+
process.exit(1);
|
| 47 |
+
});
|
scripts/migrate-data.ts
ADDED
|
@@ -0,0 +1,949 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* MongoDB to Turso Data Migration Script
|
| 3 |
+
*
|
| 4 |
+
* Migrates all data from MongoDB Atlas to Turso (SQLite).
|
| 5 |
+
* MongoDB remains READ-ONLY - no deletions or modifications.
|
| 6 |
+
*
|
| 7 |
+
* Usage:
|
| 8 |
+
* npm run migrate:data
|
| 9 |
+
* npm run migrate:data -- --dry-run
|
| 10 |
+
*
|
| 11 |
+
* Required env vars:
|
| 12 |
+
* MONGO_URL, TURSO_DATABASE_URL, TURSO_AUTH_TOKEN
|
| 13 |
+
*/
|
| 14 |
+
|
| 15 |
+
import { MongoClient, ObjectId } from "mongodb";
|
| 16 |
+
import { createClient } from "@libsql/client";
|
| 17 |
+
import { drizzle } from "drizzle-orm/libsql";
|
| 18 |
+
import { sql } from "drizzle-orm";
|
| 19 |
+
import * as schema from "../src/db/schema";
|
| 20 |
+
import { v4 as uuidv4 } from "uuid";
|
| 21 |
+
import * as dotenv from "dotenv";
|
| 22 |
+
|
| 23 |
+
dotenv.config({ path: ".env.local" });
|
| 24 |
+
|
| 25 |
+
// =============================================================================
|
| 26 |
+
// Types
|
| 27 |
+
// =============================================================================
|
| 28 |
+
|
| 29 |
+
interface MongoUser {
|
| 30 |
+
_id?: ObjectId;
|
| 31 |
+
id?: string;
|
| 32 |
+
githubId: number;
|
| 33 |
+
username: string;
|
| 34 |
+
avatarUrl: string;
|
| 35 |
+
role?: string;
|
| 36 |
+
repositories?: string[];
|
| 37 |
+
githubAccessToken?: string;
|
| 38 |
+
createdAt?: Date | string;
|
| 39 |
+
updatedAt?: Date | string;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
interface MongoRepository {
|
| 43 |
+
_id?: ObjectId;
|
| 44 |
+
id?: string;
|
| 45 |
+
githubRepoId: number;
|
| 46 |
+
name: string;
|
| 47 |
+
owner: string;
|
| 48 |
+
userId: string;
|
| 49 |
+
createdAt?: Date | string;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
interface MongoIssue {
|
| 53 |
+
_id?: ObjectId;
|
| 54 |
+
id?: string;
|
| 55 |
+
githubIssueId: number;
|
| 56 |
+
number: number;
|
| 57 |
+
title: string;
|
| 58 |
+
body?: string;
|
| 59 |
+
authorName: string;
|
| 60 |
+
repoId: string;
|
| 61 |
+
repoName: string;
|
| 62 |
+
owner?: string;
|
| 63 |
+
repo?: string;
|
| 64 |
+
htmlUrl?: string;
|
| 65 |
+
state?: string;
|
| 66 |
+
isPR?: boolean;
|
| 67 |
+
createdAt?: Date | string;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
interface MongoMessage {
|
| 71 |
+
_id?: ObjectId;
|
| 72 |
+
id?: string;
|
| 73 |
+
senderId: string;
|
| 74 |
+
receiverId: string;
|
| 75 |
+
content: string;
|
| 76 |
+
read?: boolean;
|
| 77 |
+
timestamp?: Date | string;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
interface MongoProfile {
|
| 81 |
+
_id?: ObjectId;
|
| 82 |
+
user_id: string;
|
| 83 |
+
username: string;
|
| 84 |
+
avatar_url?: string;
|
| 85 |
+
bio?: string;
|
| 86 |
+
skills?: string[];
|
| 87 |
+
location?: string;
|
| 88 |
+
website?: string;
|
| 89 |
+
twitter?: string;
|
| 90 |
+
available_for_mentoring?: boolean;
|
| 91 |
+
mentoring_topics?: string[];
|
| 92 |
+
connected_repos?: string[];
|
| 93 |
+
profile_visibility?: string;
|
| 94 |
+
show_email?: boolean;
|
| 95 |
+
github_stats?: object;
|
| 96 |
+
stats_updated_at?: Date | string;
|
| 97 |
+
created_at?: Date | string;
|
| 98 |
+
updated_at?: Date | string;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
interface MongoTriageData {
|
| 102 |
+
_id?: ObjectId;
|
| 103 |
+
id?: string;
|
| 104 |
+
issueId: string;
|
| 105 |
+
classification: string;
|
| 106 |
+
summary: string;
|
| 107 |
+
suggestedLabel: string;
|
| 108 |
+
sentiment: string;
|
| 109 |
+
analyzedAt?: Date | string;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
interface MongoTemplate {
|
| 113 |
+
_id?: ObjectId;
|
| 114 |
+
id?: string;
|
| 115 |
+
name: string;
|
| 116 |
+
body: string;
|
| 117 |
+
ownerId: string;
|
| 118 |
+
triggerClassification?: string;
|
| 119 |
+
createdAt?: Date | string;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
interface MongoChatMessage {
|
| 123 |
+
role: string;
|
| 124 |
+
content: string;
|
| 125 |
+
timestamp?: Date | string;
|
| 126 |
+
githubCommentId?: string;
|
| 127 |
+
githubCommentUrl?: string;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
interface MongoChatHistory {
|
| 131 |
+
_id?: ObjectId;
|
| 132 |
+
id?: string;
|
| 133 |
+
userId: string;
|
| 134 |
+
sessionId: string;
|
| 135 |
+
messages?: MongoChatMessage[];
|
| 136 |
+
createdAt?: Date | string;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
interface MongoMentor {
|
| 140 |
+
_id?: ObjectId;
|
| 141 |
+
id?: string;
|
| 142 |
+
userId: string;
|
| 143 |
+
username: string;
|
| 144 |
+
expertiseLevel?: string;
|
| 145 |
+
availabilityHoursPerWeek?: number;
|
| 146 |
+
timezone?: string;
|
| 147 |
+
isActive?: boolean;
|
| 148 |
+
bio?: string;
|
| 149 |
+
avatarUrl?: string;
|
| 150 |
+
techStack?: string[];
|
| 151 |
+
languages?: string[];
|
| 152 |
+
frameworks?: string[];
|
| 153 |
+
preferredTopics?: string[];
|
| 154 |
+
menteeCount?: number;
|
| 155 |
+
sessionsCompleted?: number;
|
| 156 |
+
avgRating?: number;
|
| 157 |
+
totalRatings?: number;
|
| 158 |
+
maxMentees?: number;
|
| 159 |
+
createdAt?: Date | string;
|
| 160 |
+
updatedAt?: Date | string;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
interface MongoTrophy {
|
| 164 |
+
_id?: ObjectId;
|
| 165 |
+
id?: string;
|
| 166 |
+
userId: string;
|
| 167 |
+
username: string;
|
| 168 |
+
trophyType: string;
|
| 169 |
+
name: string;
|
| 170 |
+
description: string;
|
| 171 |
+
icon: string;
|
| 172 |
+
color: string;
|
| 173 |
+
rarity: string;
|
| 174 |
+
svgData?: string;
|
| 175 |
+
isPublic?: boolean;
|
| 176 |
+
shareUrl?: string;
|
| 177 |
+
earnedFor?: string;
|
| 178 |
+
milestoneValue?: number;
|
| 179 |
+
awardedAt?: Date | string;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
interface MongoIssueChat {
|
| 183 |
+
_id?: ObjectId;
|
| 184 |
+
id?: string;
|
| 185 |
+
issueId: string;
|
| 186 |
+
userId: string;
|
| 187 |
+
sessionId: string;
|
| 188 |
+
messages?: MongoChatMessage[];
|
| 189 |
+
createdAt?: Date | string;
|
| 190 |
+
updatedAt?: Date | string;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
interface MongoResource {
|
| 194 |
+
_id?: ObjectId;
|
| 195 |
+
id?: string;
|
| 196 |
+
repoName: string;
|
| 197 |
+
sourceType?: string;
|
| 198 |
+
sourceId?: string;
|
| 199 |
+
resourceType: string;
|
| 200 |
+
title: string;
|
| 201 |
+
content: string;
|
| 202 |
+
description?: string;
|
| 203 |
+
language?: string;
|
| 204 |
+
sharedBy: string;
|
| 205 |
+
sharedById: string;
|
| 206 |
+
tags?: string[];
|
| 207 |
+
saveCount?: number;
|
| 208 |
+
helpfulCount?: number;
|
| 209 |
+
createdAt?: Date | string;
|
| 210 |
+
updatedAt?: Date | string;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
// =============================================================================
|
| 214 |
+
// Utilities
|
| 215 |
+
// =============================================================================
|
| 216 |
+
|
| 217 |
+
function toIsoString(date: Date | string | undefined): string {
|
| 218 |
+
if (!date) return new Date().toISOString();
|
| 219 |
+
if (typeof date === "string") return date;
|
| 220 |
+
return date.toISOString();
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
function extractId(doc: { _id?: ObjectId; id?: string }): string {
|
| 224 |
+
if (doc.id) return doc.id;
|
| 225 |
+
if (doc._id) return doc._id.toString();
|
| 226 |
+
return uuidv4();
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
function log(message: string, type: "info" | "success" | "error" | "warn" = "info") {
|
| 230 |
+
const icons = { info: "ℹ️", success: "✅", error: "❌", warn: "⚠️" };
|
| 231 |
+
const time = new Date().toISOString().split("T")[1].split(".")[0];
|
| 232 |
+
console.log(`[${time}] ${icons[type]} ${message}`);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
// =============================================================================
|
| 236 |
+
// Migration Functions
|
| 237 |
+
// =============================================================================
|
| 238 |
+
|
| 239 |
+
async function migrateUsers(
|
| 240 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 241 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 242 |
+
isDryRun: boolean
|
| 243 |
+
): Promise<Map<string, string>> {
|
| 244 |
+
log("Migrating Users...");
|
| 245 |
+
const userIdMap = new Map<string, string>();
|
| 246 |
+
const users = await mongoDb.collection<MongoUser>("users").find().toArray();
|
| 247 |
+
log(`Found ${users.length} users`);
|
| 248 |
+
|
| 249 |
+
let success = 0, skipped = 0;
|
| 250 |
+
for (const mongoUser of users) {
|
| 251 |
+
const userId = extractId(mongoUser);
|
| 252 |
+
const originalId = mongoUser._id?.toString() || mongoUser.id || "";
|
| 253 |
+
userIdMap.set(originalId, userId);
|
| 254 |
+
|
| 255 |
+
if (!isDryRun) {
|
| 256 |
+
try {
|
| 257 |
+
await tursoDb.insert(schema.users).values({
|
| 258 |
+
id: userId,
|
| 259 |
+
githubId: mongoUser.githubId,
|
| 260 |
+
username: mongoUser.username,
|
| 261 |
+
avatarUrl: mongoUser.avatarUrl,
|
| 262 |
+
role: mongoUser.role || null,
|
| 263 |
+
githubAccessToken: mongoUser.githubAccessToken || null,
|
| 264 |
+
createdAt: toIsoString(mongoUser.createdAt),
|
| 265 |
+
updatedAt: toIsoString(mongoUser.updatedAt),
|
| 266 |
+
}).onConflictDoNothing();
|
| 267 |
+
|
| 268 |
+
// Migrate user repositories array
|
| 269 |
+
if (mongoUser.repositories?.length) {
|
| 270 |
+
for (const repoName of mongoUser.repositories) {
|
| 271 |
+
await tursoDb.insert(schema.userRepositories).values({
|
| 272 |
+
id: uuidv4(),
|
| 273 |
+
userId: userId,
|
| 274 |
+
repoFullName: repoName,
|
| 275 |
+
addedAt: toIsoString(mongoUser.createdAt),
|
| 276 |
+
}).onConflictDoNothing();
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
success++;
|
| 280 |
+
} catch (e) {
|
| 281 |
+
skipped++;
|
| 282 |
+
}
|
| 283 |
+
} else {
|
| 284 |
+
log(`[DRY] User: ${mongoUser.username}`);
|
| 285 |
+
success++;
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
log(`Users: ${success} migrated, ${skipped} skipped`, "success");
|
| 289 |
+
return userIdMap;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
async function migrateRepositories(
|
| 293 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 294 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 295 |
+
userIdMap: Map<string, string>,
|
| 296 |
+
isDryRun: boolean
|
| 297 |
+
): Promise<Map<string, string>> {
|
| 298 |
+
log("Migrating Repositories...");
|
| 299 |
+
const repoIdMap = new Map<string, string>();
|
| 300 |
+
const repos = await mongoDb.collection<MongoRepository>("repositories").find().toArray();
|
| 301 |
+
log(`Found ${repos.length} repositories`);
|
| 302 |
+
|
| 303 |
+
let success = 0, skipped = 0;
|
| 304 |
+
for (const repo of repos) {
|
| 305 |
+
const repoId = extractId(repo);
|
| 306 |
+
const originalId = repo._id?.toString() || repo.id || "";
|
| 307 |
+
repoIdMap.set(originalId, repoId);
|
| 308 |
+
|
| 309 |
+
const mappedUserId = userIdMap.get(repo.userId) || repo.userId;
|
| 310 |
+
|
| 311 |
+
if (!isDryRun) {
|
| 312 |
+
try {
|
| 313 |
+
await tursoDb.insert(schema.repositories).values({
|
| 314 |
+
id: repoId,
|
| 315 |
+
githubRepoId: repo.githubRepoId,
|
| 316 |
+
name: repo.name,
|
| 317 |
+
owner: repo.owner,
|
| 318 |
+
userId: mappedUserId,
|
| 319 |
+
createdAt: toIsoString(repo.createdAt),
|
| 320 |
+
}).onConflictDoNothing();
|
| 321 |
+
success++;
|
| 322 |
+
} catch (e) {
|
| 323 |
+
skipped++;
|
| 324 |
+
}
|
| 325 |
+
} else {
|
| 326 |
+
log(`[DRY] Repo: ${repo.owner}/${repo.name}`);
|
| 327 |
+
success++;
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
log(`Repositories: ${success} migrated, ${skipped} skipped`, "success");
|
| 331 |
+
return repoIdMap;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
async function migrateIssues(
|
| 335 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 336 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 337 |
+
repoIdMap: Map<string, string>,
|
| 338 |
+
isDryRun: boolean
|
| 339 |
+
): Promise<Map<string, string>> {
|
| 340 |
+
log("Migrating Issues...");
|
| 341 |
+
const issueIdMap = new Map<string, string>();
|
| 342 |
+
const issues = await mongoDb.collection<MongoIssue>("issues").find().toArray();
|
| 343 |
+
log(`Found ${issues.length} issues`);
|
| 344 |
+
|
| 345 |
+
let success = 0, skipped = 0;
|
| 346 |
+
for (const issue of issues) {
|
| 347 |
+
const issueId = extractId(issue);
|
| 348 |
+
const originalId = issue._id?.toString() || issue.id || "";
|
| 349 |
+
issueIdMap.set(originalId, issueId);
|
| 350 |
+
|
| 351 |
+
const mappedRepoId = repoIdMap.get(issue.repoId) || issue.repoId;
|
| 352 |
+
|
| 353 |
+
if (!isDryRun) {
|
| 354 |
+
try {
|
| 355 |
+
await tursoDb.insert(schema.issues).values({
|
| 356 |
+
id: issueId,
|
| 357 |
+
githubIssueId: issue.githubIssueId,
|
| 358 |
+
number: issue.number,
|
| 359 |
+
title: issue.title,
|
| 360 |
+
body: issue.body || null,
|
| 361 |
+
authorName: issue.authorName,
|
| 362 |
+
repoId: mappedRepoId,
|
| 363 |
+
repoName: issue.repoName,
|
| 364 |
+
owner: issue.owner || null,
|
| 365 |
+
repo: issue.repo || null,
|
| 366 |
+
htmlUrl: issue.htmlUrl || null,
|
| 367 |
+
state: issue.state || "open",
|
| 368 |
+
isPR: issue.isPR || false,
|
| 369 |
+
createdAt: toIsoString(issue.createdAt),
|
| 370 |
+
}).onConflictDoNothing();
|
| 371 |
+
success++;
|
| 372 |
+
} catch (e) {
|
| 373 |
+
skipped++;
|
| 374 |
+
}
|
| 375 |
+
} else {
|
| 376 |
+
log(`[DRY] Issue: #${issue.number} - ${issue.title.substring(0, 30)}...`);
|
| 377 |
+
success++;
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
log(`Issues: ${success} migrated, ${skipped} skipped`, "success");
|
| 381 |
+
return issueIdMap;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
async function migrateMessages(
|
| 385 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 386 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 387 |
+
userIdMap: Map<string, string>,
|
| 388 |
+
isDryRun: boolean
|
| 389 |
+
): Promise<void> {
|
| 390 |
+
log("Migrating Messages...");
|
| 391 |
+
const messages = await mongoDb.collection<MongoMessage>("messages").find().toArray();
|
| 392 |
+
log(`Found ${messages.length} messages`);
|
| 393 |
+
|
| 394 |
+
let success = 0, skipped = 0;
|
| 395 |
+
for (const msg of messages) {
|
| 396 |
+
const msgId = extractId(msg);
|
| 397 |
+
const senderId = userIdMap.get(msg.senderId) || msg.senderId;
|
| 398 |
+
const receiverId = userIdMap.get(msg.receiverId) || msg.receiverId;
|
| 399 |
+
|
| 400 |
+
if (!isDryRun) {
|
| 401 |
+
try {
|
| 402 |
+
await tursoDb.insert(schema.messages).values({
|
| 403 |
+
id: msgId,
|
| 404 |
+
senderId: senderId,
|
| 405 |
+
receiverId: receiverId,
|
| 406 |
+
content: msg.content,
|
| 407 |
+
read: msg.read || false,
|
| 408 |
+
timestamp: toIsoString(msg.timestamp),
|
| 409 |
+
}).onConflictDoNothing();
|
| 410 |
+
success++;
|
| 411 |
+
} catch (e) {
|
| 412 |
+
skipped++;
|
| 413 |
+
}
|
| 414 |
+
} else {
|
| 415 |
+
success++;
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
log(`Messages: ${success} migrated, ${skipped} skipped`, "success");
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
async function migrateProfiles(
|
| 422 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 423 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 424 |
+
userIdMap: Map<string, string>,
|
| 425 |
+
isDryRun: boolean
|
| 426 |
+
): Promise<void> {
|
| 427 |
+
log("Migrating Profiles...");
|
| 428 |
+
const profiles = await mongoDb.collection<MongoProfile>("profiles").find().toArray();
|
| 429 |
+
log(`Found ${profiles.length} profiles`);
|
| 430 |
+
|
| 431 |
+
let success = 0, skipped = 0;
|
| 432 |
+
for (const profile of profiles) {
|
| 433 |
+
const userId = userIdMap.get(profile.user_id) || profile.user_id;
|
| 434 |
+
|
| 435 |
+
if (!isDryRun) {
|
| 436 |
+
try {
|
| 437 |
+
await tursoDb.insert(schema.profiles).values({
|
| 438 |
+
userId: userId,
|
| 439 |
+
username: profile.username,
|
| 440 |
+
avatarUrl: profile.avatar_url || null,
|
| 441 |
+
bio: profile.bio || null,
|
| 442 |
+
location: profile.location || null,
|
| 443 |
+
website: profile.website || null,
|
| 444 |
+
twitter: profile.twitter || null,
|
| 445 |
+
availableForMentoring: profile.available_for_mentoring || false,
|
| 446 |
+
profileVisibility: profile.profile_visibility || "public",
|
| 447 |
+
showEmail: profile.show_email || false,
|
| 448 |
+
githubStats: profile.github_stats ? JSON.stringify(profile.github_stats) : null,
|
| 449 |
+
statsUpdatedAt: profile.stats_updated_at ? toIsoString(profile.stats_updated_at) : null,
|
| 450 |
+
createdAt: toIsoString(profile.created_at),
|
| 451 |
+
updatedAt: toIsoString(profile.updated_at),
|
| 452 |
+
}).onConflictDoNothing();
|
| 453 |
+
|
| 454 |
+
// Migrate skills
|
| 455 |
+
if (profile.skills?.length) {
|
| 456 |
+
for (const skill of profile.skills) {
|
| 457 |
+
await tursoDb.insert(schema.profileSkills).values({
|
| 458 |
+
profileId: userId,
|
| 459 |
+
skill: skill,
|
| 460 |
+
}).onConflictDoNothing();
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
// Migrate mentoring topics
|
| 465 |
+
if (profile.mentoring_topics?.length) {
|
| 466 |
+
for (const topic of profile.mentoring_topics) {
|
| 467 |
+
await tursoDb.insert(schema.profileMentoringTopics).values({
|
| 468 |
+
profileId: userId,
|
| 469 |
+
topic: topic,
|
| 470 |
+
}).onConflictDoNothing();
|
| 471 |
+
}
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
// Migrate connected repos
|
| 475 |
+
if (profile.connected_repos?.length) {
|
| 476 |
+
for (const repo of profile.connected_repos) {
|
| 477 |
+
await tursoDb.insert(schema.profileConnectedRepos).values({
|
| 478 |
+
profileId: userId,
|
| 479 |
+
repoName: repo,
|
| 480 |
+
}).onConflictDoNothing();
|
| 481 |
+
}
|
| 482 |
+
}
|
| 483 |
+
success++;
|
| 484 |
+
} catch (e) {
|
| 485 |
+
skipped++;
|
| 486 |
+
}
|
| 487 |
+
} else {
|
| 488 |
+
success++;
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
log(`Profiles: ${success} migrated, ${skipped} skipped`, "success");
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
async function migrateTriageData(
|
| 495 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 496 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 497 |
+
issueIdMap: Map<string, string>,
|
| 498 |
+
isDryRun: boolean
|
| 499 |
+
): Promise<void> {
|
| 500 |
+
log("Migrating Triage Data...");
|
| 501 |
+
const triageData = await mongoDb.collection<MongoTriageData>("triageData").find().toArray();
|
| 502 |
+
log(`Found ${triageData.length} triage records`);
|
| 503 |
+
|
| 504 |
+
let success = 0, skipped = 0;
|
| 505 |
+
for (const triage of triageData) {
|
| 506 |
+
const triageId = extractId(triage);
|
| 507 |
+
const issueId = issueIdMap.get(triage.issueId) || triage.issueId;
|
| 508 |
+
|
| 509 |
+
if (!isDryRun) {
|
| 510 |
+
try {
|
| 511 |
+
await tursoDb.insert(schema.triageData).values({
|
| 512 |
+
id: triageId,
|
| 513 |
+
issueId: issueId,
|
| 514 |
+
classification: triage.classification,
|
| 515 |
+
summary: triage.summary,
|
| 516 |
+
suggestedLabel: triage.suggestedLabel,
|
| 517 |
+
sentiment: triage.sentiment,
|
| 518 |
+
analyzedAt: toIsoString(triage.analyzedAt),
|
| 519 |
+
}).onConflictDoNothing();
|
| 520 |
+
success++;
|
| 521 |
+
} catch (e) {
|
| 522 |
+
skipped++;
|
| 523 |
+
}
|
| 524 |
+
} else {
|
| 525 |
+
success++;
|
| 526 |
+
}
|
| 527 |
+
}
|
| 528 |
+
log(`Triage Data: ${success} migrated, ${skipped} skipped`, "success");
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
async function migrateTemplates(
|
| 532 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 533 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 534 |
+
userIdMap: Map<string, string>,
|
| 535 |
+
isDryRun: boolean
|
| 536 |
+
): Promise<void> {
|
| 537 |
+
log("Migrating Templates...");
|
| 538 |
+
const templates = await mongoDb.collection<MongoTemplate>("templates").find().toArray();
|
| 539 |
+
log(`Found ${templates.length} templates`);
|
| 540 |
+
|
| 541 |
+
let success = 0, skipped = 0;
|
| 542 |
+
for (const template of templates) {
|
| 543 |
+
const templateId = extractId(template);
|
| 544 |
+
const ownerId = userIdMap.get(template.ownerId) || template.ownerId;
|
| 545 |
+
|
| 546 |
+
if (!isDryRun) {
|
| 547 |
+
try {
|
| 548 |
+
await tursoDb.insert(schema.templates).values({
|
| 549 |
+
id: templateId,
|
| 550 |
+
name: template.name,
|
| 551 |
+
body: template.body,
|
| 552 |
+
ownerId: ownerId,
|
| 553 |
+
triggerClassification: template.triggerClassification || null,
|
| 554 |
+
createdAt: toIsoString(template.createdAt),
|
| 555 |
+
}).onConflictDoNothing();
|
| 556 |
+
success++;
|
| 557 |
+
} catch (e) {
|
| 558 |
+
skipped++;
|
| 559 |
+
}
|
| 560 |
+
} else {
|
| 561 |
+
success++;
|
| 562 |
+
}
|
| 563 |
+
}
|
| 564 |
+
log(`Templates: ${success} migrated, ${skipped} skipped`, "success");
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
async function migrateChatHistory(
|
| 568 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 569 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 570 |
+
userIdMap: Map<string, string>,
|
| 571 |
+
isDryRun: boolean
|
| 572 |
+
): Promise<void> {
|
| 573 |
+
log("Migrating Chat History...");
|
| 574 |
+
const chatHistories = await mongoDb.collection<MongoChatHistory>("chat_history").find().toArray();
|
| 575 |
+
log(`Found ${chatHistories.length} chat histories`);
|
| 576 |
+
|
| 577 |
+
let historySuccess = 0, historySkipped = 0;
|
| 578 |
+
let messageSuccess = 0, messageSkipped = 0;
|
| 579 |
+
|
| 580 |
+
for (const history of chatHistories) {
|
| 581 |
+
const historyId = extractId(history);
|
| 582 |
+
const userId = userIdMap.get(history.userId) || history.userId;
|
| 583 |
+
|
| 584 |
+
if (!isDryRun) {
|
| 585 |
+
try {
|
| 586 |
+
// Insert chat history record
|
| 587 |
+
await tursoDb.insert(schema.chatHistory).values({
|
| 588 |
+
id: historyId,
|
| 589 |
+
userId: userId,
|
| 590 |
+
sessionId: history.sessionId,
|
| 591 |
+
createdAt: toIsoString(history.createdAt),
|
| 592 |
+
}).onConflictDoNothing();
|
| 593 |
+
historySuccess++;
|
| 594 |
+
|
| 595 |
+
// Insert related messages
|
| 596 |
+
if (history.messages?.length) {
|
| 597 |
+
for (const msg of history.messages) {
|
| 598 |
+
const msgId = uuidv4();
|
| 599 |
+
try {
|
| 600 |
+
await tursoDb.insert(schema.chatHistoryMessages).values({
|
| 601 |
+
id: msgId,
|
| 602 |
+
chatHistoryId: historyId,
|
| 603 |
+
role: msg.role,
|
| 604 |
+
content: msg.content,
|
| 605 |
+
timestamp: toIsoString(msg.timestamp),
|
| 606 |
+
githubCommentId: msg.githubCommentId || null,
|
| 607 |
+
githubCommentUrl: msg.githubCommentUrl || null,
|
| 608 |
+
}).onConflictDoNothing();
|
| 609 |
+
messageSuccess++;
|
| 610 |
+
} catch (e) {
|
| 611 |
+
messageSkipped++;
|
| 612 |
+
}
|
| 613 |
+
}
|
| 614 |
+
}
|
| 615 |
+
} catch (e) {
|
| 616 |
+
historySkipped++;
|
| 617 |
+
}
|
| 618 |
+
} else {
|
| 619 |
+
log(`[DRY] Chat History: ${historyId} with ${history.messages?.length || 0} messages`);
|
| 620 |
+
historySuccess++;
|
| 621 |
+
messageSuccess += history.messages?.length || 0;
|
| 622 |
+
}
|
| 623 |
+
}
|
| 624 |
+
log(`Chat History: ${historySuccess} migrated, ${historySkipped} skipped`, "success");
|
| 625 |
+
log(`Chat Messages: ${messageSuccess} migrated, ${messageSkipped} skipped`, "success");
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
async function migrateMentors(
|
| 629 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 630 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 631 |
+
userIdMap: Map<string, string>,
|
| 632 |
+
isDryRun: boolean
|
| 633 |
+
): Promise<Map<string, string>> {
|
| 634 |
+
log("Migrating Mentors...");
|
| 635 |
+
const mentorIdMap = new Map<string, string>();
|
| 636 |
+
const mentors = await mongoDb.collection<MongoMentor>("mentors").find().toArray();
|
| 637 |
+
log(`Found ${mentors.length} mentors`);
|
| 638 |
+
|
| 639 |
+
let success = 0, skipped = 0;
|
| 640 |
+
for (const mentor of mentors) {
|
| 641 |
+
const mentorId = extractId(mentor);
|
| 642 |
+
const originalId = mentor._id?.toString() || mentor.id || "";
|
| 643 |
+
mentorIdMap.set(originalId, mentorId);
|
| 644 |
+
|
| 645 |
+
const mappedUserId = userIdMap.get(mentor.userId) || mentor.userId;
|
| 646 |
+
|
| 647 |
+
if (!isDryRun) {
|
| 648 |
+
try {
|
| 649 |
+
await tursoDb.insert(schema.mentors).values({
|
| 650 |
+
id: mentorId,
|
| 651 |
+
userId: mappedUserId,
|
| 652 |
+
username: mentor.username,
|
| 653 |
+
expertiseLevel: mentor.expertiseLevel || "intermediate",
|
| 654 |
+
availabilityHoursPerWeek: mentor.availabilityHoursPerWeek || 5,
|
| 655 |
+
timezone: mentor.timezone || null,
|
| 656 |
+
isActive: mentor.isActive ?? true,
|
| 657 |
+
bio: mentor.bio || null,
|
| 658 |
+
avatarUrl: mentor.avatarUrl || null,
|
| 659 |
+
menteeCount: mentor.menteeCount || 0,
|
| 660 |
+
sessionsCompleted: mentor.sessionsCompleted || 0,
|
| 661 |
+
avgRating: mentor.avgRating || 0,
|
| 662 |
+
totalRatings: mentor.totalRatings || 0,
|
| 663 |
+
maxMentees: mentor.maxMentees || 3,
|
| 664 |
+
createdAt: toIsoString(mentor.createdAt),
|
| 665 |
+
updatedAt: toIsoString(mentor.updatedAt),
|
| 666 |
+
}).onConflictDoNothing();
|
| 667 |
+
|
| 668 |
+
// Migrate tech stack
|
| 669 |
+
if (mentor.techStack?.length) {
|
| 670 |
+
for (const tech of mentor.techStack) {
|
| 671 |
+
await tursoDb.insert(schema.mentorTechStack).values({
|
| 672 |
+
mentorId: mentorId,
|
| 673 |
+
tech: tech,
|
| 674 |
+
}).onConflictDoNothing();
|
| 675 |
+
}
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
// Migrate languages
|
| 679 |
+
if (mentor.languages?.length) {
|
| 680 |
+
for (const lang of mentor.languages) {
|
| 681 |
+
await tursoDb.insert(schema.mentorLanguages).values({
|
| 682 |
+
mentorId: mentorId,
|
| 683 |
+
language: lang,
|
| 684 |
+
}).onConflictDoNothing();
|
| 685 |
+
}
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
// Migrate frameworks
|
| 689 |
+
if (mentor.frameworks?.length) {
|
| 690 |
+
for (const fw of mentor.frameworks) {
|
| 691 |
+
await tursoDb.insert(schema.mentorFrameworks).values({
|
| 692 |
+
mentorId: mentorId,
|
| 693 |
+
framework: fw,
|
| 694 |
+
}).onConflictDoNothing();
|
| 695 |
+
}
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
// Migrate preferred topics
|
| 699 |
+
if (mentor.preferredTopics?.length) {
|
| 700 |
+
for (const topic of mentor.preferredTopics) {
|
| 701 |
+
await tursoDb.insert(schema.mentorPreferredTopics).values({
|
| 702 |
+
mentorId: mentorId,
|
| 703 |
+
topic: topic,
|
| 704 |
+
}).onConflictDoNothing();
|
| 705 |
+
}
|
| 706 |
+
}
|
| 707 |
+
success++;
|
| 708 |
+
} catch (e) {
|
| 709 |
+
skipped++;
|
| 710 |
+
}
|
| 711 |
+
} else {
|
| 712 |
+
log(`[DRY] Mentor: ${mentor.username}`);
|
| 713 |
+
success++;
|
| 714 |
+
}
|
| 715 |
+
}
|
| 716 |
+
log(`Mentors: ${success} migrated, ${skipped} skipped`, "success");
|
| 717 |
+
return mentorIdMap;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
async function migrateTrophies(
|
| 721 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 722 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 723 |
+
userIdMap: Map<string, string>,
|
| 724 |
+
isDryRun: boolean
|
| 725 |
+
): Promise<void> {
|
| 726 |
+
log("Migrating Trophies...");
|
| 727 |
+
const trophies = await mongoDb.collection<MongoTrophy>("trophies").find().toArray();
|
| 728 |
+
log(`Found ${trophies.length} trophies`);
|
| 729 |
+
|
| 730 |
+
let success = 0, skipped = 0;
|
| 731 |
+
for (const trophy of trophies) {
|
| 732 |
+
const trophyId = extractId(trophy);
|
| 733 |
+
const mappedUserId = userIdMap.get(trophy.userId) || trophy.userId;
|
| 734 |
+
|
| 735 |
+
if (!isDryRun) {
|
| 736 |
+
try {
|
| 737 |
+
await tursoDb.insert(schema.trophies).values({
|
| 738 |
+
id: trophyId,
|
| 739 |
+
userId: mappedUserId,
|
| 740 |
+
username: trophy.username,
|
| 741 |
+
trophyType: trophy.trophyType,
|
| 742 |
+
name: trophy.name,
|
| 743 |
+
description: trophy.description,
|
| 744 |
+
icon: trophy.icon,
|
| 745 |
+
color: trophy.color,
|
| 746 |
+
rarity: trophy.rarity,
|
| 747 |
+
svgData: trophy.svgData || null,
|
| 748 |
+
isPublic: trophy.isPublic ?? true,
|
| 749 |
+
shareUrl: trophy.shareUrl || null,
|
| 750 |
+
earnedFor: trophy.earnedFor || null,
|
| 751 |
+
milestoneValue: trophy.milestoneValue || null,
|
| 752 |
+
awardedAt: toIsoString(trophy.awardedAt),
|
| 753 |
+
}).onConflictDoNothing();
|
| 754 |
+
success++;
|
| 755 |
+
} catch (e) {
|
| 756 |
+
skipped++;
|
| 757 |
+
}
|
| 758 |
+
} else {
|
| 759 |
+
log(`[DRY] Trophy: ${trophy.name} for ${trophy.username}`);
|
| 760 |
+
success++;
|
| 761 |
+
}
|
| 762 |
+
}
|
| 763 |
+
log(`Trophies: ${success} migrated, ${skipped} skipped`, "success");
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
async function migrateIssueChats(
|
| 767 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 768 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 769 |
+
userIdMap: Map<string, string>,
|
| 770 |
+
issueIdMap: Map<string, string>,
|
| 771 |
+
isDryRun: boolean
|
| 772 |
+
): Promise<void> {
|
| 773 |
+
log("Migrating Issue Chats...");
|
| 774 |
+
const issueChats = await mongoDb.collection<MongoIssueChat>("issue_chats").find().toArray();
|
| 775 |
+
log(`Found ${issueChats.length} issue chats`);
|
| 776 |
+
|
| 777 |
+
let chatSuccess = 0, chatSkipped = 0;
|
| 778 |
+
let msgSuccess = 0, msgSkipped = 0;
|
| 779 |
+
|
| 780 |
+
for (const chat of issueChats) {
|
| 781 |
+
const chatId = extractId(chat);
|
| 782 |
+
const mappedUserId = userIdMap.get(chat.userId) || chat.userId;
|
| 783 |
+
const mappedIssueId = issueIdMap.get(chat.issueId) || chat.issueId;
|
| 784 |
+
|
| 785 |
+
if (!isDryRun) {
|
| 786 |
+
try {
|
| 787 |
+
await tursoDb.insert(schema.issueChats).values({
|
| 788 |
+
id: chatId,
|
| 789 |
+
issueId: mappedIssueId,
|
| 790 |
+
userId: mappedUserId,
|
| 791 |
+
sessionId: chat.sessionId,
|
| 792 |
+
createdAt: toIsoString(chat.createdAt),
|
| 793 |
+
updatedAt: toIsoString(chat.updatedAt),
|
| 794 |
+
}).onConflictDoNothing();
|
| 795 |
+
chatSuccess++;
|
| 796 |
+
|
| 797 |
+
// Migrate messages
|
| 798 |
+
if (chat.messages?.length) {
|
| 799 |
+
for (const msg of chat.messages) {
|
| 800 |
+
const msgId = uuidv4();
|
| 801 |
+
try {
|
| 802 |
+
await tursoDb.insert(schema.issueChatMessages).values({
|
| 803 |
+
id: msgId,
|
| 804 |
+
issueChatId: chatId,
|
| 805 |
+
role: msg.role,
|
| 806 |
+
content: msg.content,
|
| 807 |
+
timestamp: toIsoString(msg.timestamp),
|
| 808 |
+
githubCommentId: msg.githubCommentId || null,
|
| 809 |
+
githubCommentUrl: msg.githubCommentUrl || null,
|
| 810 |
+
}).onConflictDoNothing();
|
| 811 |
+
msgSuccess++;
|
| 812 |
+
} catch (e) {
|
| 813 |
+
msgSkipped++;
|
| 814 |
+
}
|
| 815 |
+
}
|
| 816 |
+
}
|
| 817 |
+
} catch (e) {
|
| 818 |
+
chatSkipped++;
|
| 819 |
+
}
|
| 820 |
+
} else {
|
| 821 |
+
log(`[DRY] Issue Chat: ${chatId} with ${chat.messages?.length || 0} messages`);
|
| 822 |
+
chatSuccess++;
|
| 823 |
+
msgSuccess += chat.messages?.length || 0;
|
| 824 |
+
}
|
| 825 |
+
}
|
| 826 |
+
log(`Issue Chats: ${chatSuccess} migrated, ${chatSkipped} skipped`, "success");
|
| 827 |
+
log(`Issue Chat Messages: ${msgSuccess} migrated, ${msgSkipped} skipped`, "success");
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
async function migrateResources(
|
| 831 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 832 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 833 |
+
userIdMap: Map<string, string>,
|
| 834 |
+
isDryRun: boolean
|
| 835 |
+
): Promise<void> {
|
| 836 |
+
log("Migrating Resources...");
|
| 837 |
+
const resources = await mongoDb.collection<MongoResource>("resources").find().toArray();
|
| 838 |
+
log(`Found ${resources.length} resources`);
|
| 839 |
+
|
| 840 |
+
let success = 0, skipped = 0;
|
| 841 |
+
for (const resource of resources) {
|
| 842 |
+
const resourceId = extractId(resource);
|
| 843 |
+
const mappedUserId = userIdMap.get(resource.sharedById) || resource.sharedById;
|
| 844 |
+
|
| 845 |
+
if (!isDryRun) {
|
| 846 |
+
try {
|
| 847 |
+
await tursoDb.insert(schema.resources).values({
|
| 848 |
+
id: resourceId,
|
| 849 |
+
repoName: resource.repoName,
|
| 850 |
+
sourceType: resource.sourceType || "chat",
|
| 851 |
+
sourceId: resource.sourceId || null,
|
| 852 |
+
resourceType: resource.resourceType,
|
| 853 |
+
title: resource.title,
|
| 854 |
+
content: resource.content,
|
| 855 |
+
description: resource.description || null,
|
| 856 |
+
language: resource.language || null,
|
| 857 |
+
sharedBy: resource.sharedBy,
|
| 858 |
+
sharedById: mappedUserId,
|
| 859 |
+
saveCount: resource.saveCount || 0,
|
| 860 |
+
helpfulCount: resource.helpfulCount || 0,
|
| 861 |
+
createdAt: toIsoString(resource.createdAt),
|
| 862 |
+
updatedAt: toIsoString(resource.updatedAt),
|
| 863 |
+
}).onConflictDoNothing();
|
| 864 |
+
|
| 865 |
+
// Migrate tags
|
| 866 |
+
if (resource.tags?.length) {
|
| 867 |
+
for (const tag of resource.tags) {
|
| 868 |
+
await tursoDb.insert(schema.resourceTags).values({
|
| 869 |
+
resourceId: resourceId,
|
| 870 |
+
tag: tag,
|
| 871 |
+
}).onConflictDoNothing();
|
| 872 |
+
}
|
| 873 |
+
}
|
| 874 |
+
success++;
|
| 875 |
+
} catch (e) {
|
| 876 |
+
skipped++;
|
| 877 |
+
}
|
| 878 |
+
} else {
|
| 879 |
+
log(`[DRY] Resource: ${resource.title}`);
|
| 880 |
+
success++;
|
| 881 |
+
}
|
| 882 |
+
}
|
| 883 |
+
log(`Resources: ${success} migrated, ${skipped} skipped`, "success");
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
// =============================================================================
|
| 887 |
+
// Main
|
| 888 |
+
// =============================================================================
|
| 889 |
+
|
| 890 |
+
async function main() {
|
| 891 |
+
const args = process.argv.slice(2);
|
| 892 |
+
const isDryRun = args.includes("--dry-run");
|
| 893 |
+
|
| 894 |
+
if (isDryRun) {
|
| 895 |
+
log("=== DRY RUN MODE ===", "warn");
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
const mongoUri = process.env.MONGO_URL || process.env.MONGODB_URI;
|
| 899 |
+
const tursoUrl = process.env.TURSO_DATABASE_URL;
|
| 900 |
+
const tursoToken = process.env.TURSO_AUTH_TOKEN;
|
| 901 |
+
|
| 902 |
+
if (!mongoUri) {
|
| 903 |
+
log("Missing MONGO_URL or MONGODB_URI", "error");
|
| 904 |
+
process.exit(1);
|
| 905 |
+
}
|
| 906 |
+
if (!tursoUrl) {
|
| 907 |
+
log("Missing TURSO_DATABASE_URL", "error");
|
| 908 |
+
process.exit(1);
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
log("Connecting to MongoDB...");
|
| 912 |
+
const mongoClient = new MongoClient(mongoUri);
|
| 913 |
+
await mongoClient.connect();
|
| 914 |
+
const dbName = process.env.DB_NAME || "opentriage_db";
|
| 915 |
+
const mongoDb = mongoClient.db(dbName);
|
| 916 |
+
log(`Connected to MongoDB (${dbName})`, "success");
|
| 917 |
+
|
| 918 |
+
log("Connecting to Turso...");
|
| 919 |
+
const tursoClient = createClient({ url: tursoUrl, authToken: tursoToken });
|
| 920 |
+
const tursoDb = drizzle(tursoClient, { schema });
|
| 921 |
+
log("Connected to Turso", "success");
|
| 922 |
+
|
| 923 |
+
try {
|
| 924 |
+
// Migrate in order (respecting foreign keys)
|
| 925 |
+
const userIdMap = await migrateUsers(mongoDb, tursoDb, isDryRun);
|
| 926 |
+
const repoIdMap = await migrateRepositories(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 927 |
+
const issueIdMap = await migrateIssues(mongoDb, tursoDb, repoIdMap, isDryRun);
|
| 928 |
+
await migrateMessages(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 929 |
+
await migrateProfiles(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 930 |
+
await migrateTriageData(mongoDb, tursoDb, issueIdMap, isDryRun);
|
| 931 |
+
await migrateTemplates(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 932 |
+
await migrateChatHistory(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 933 |
+
|
| 934 |
+
// New migrations for complete feature support
|
| 935 |
+
const mentorIdMap = await migrateMentors(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 936 |
+
await migrateTrophies(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 937 |
+
await migrateIssueChats(mongoDb, tursoDb, userIdMap, issueIdMap, isDryRun);
|
| 938 |
+
await migrateResources(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 939 |
+
|
| 940 |
+
log("=== Migration Complete ===", "success");
|
| 941 |
+
if (isDryRun) {
|
| 942 |
+
log("Run without --dry-run to perform actual migration", "info");
|
| 943 |
+
}
|
| 944 |
+
} finally {
|
| 945 |
+
await mongoClient.close();
|
| 946 |
+
}
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
main().catch(console.error);
|
src/lib/github-contributions.ts
CHANGED
|
@@ -112,6 +112,10 @@ export async function fetchGitHubContributions(
|
|
| 112 |
return null;
|
| 113 |
}
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
const result = await response.json();
|
| 116 |
|
| 117 |
if (result.errors) {
|
|
|
|
| 112 |
return null;
|
| 113 |
}
|
| 114 |
|
| 115 |
+
// Log the token scopes for debugging
|
| 116 |
+
const scopesHeader = response.headers.get('x-oauth-scopes');
|
| 117 |
+
console.log(`[GitHub] Token scopes: ${scopesHeader}`);
|
| 118 |
+
|
| 119 |
const result = await response.json();
|
| 120 |
|
| 121 |
if (result.errors) {
|