KrishnaCosmic commited on
Commit
af9aabf
·
1 Parent(s): 8483276

fix: badges save/check, year selector for contributions

Browse files
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) {