KrishnaCosmic commited on
Commit
3eab45c
·
1 Parent(s): a061734

Add missing API endpoints for contributor, messaging, and RAG

Browse files
scripts/migrate-data.ts CHANGED
@@ -136,6 +136,80 @@ interface MongoChatHistory {
136
  createdAt?: Date | string;
137
  }
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  // =============================================================================
140
  // Utilities
141
  // =============================================================================
@@ -551,6 +625,264 @@ async function migrateChatHistory(
551
  log(`Chat Messages: ${messageSuccess} migrated, ${messageSkipped} skipped`, "success");
552
  }
553
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  // =============================================================================
555
  // Main
556
  // =============================================================================
@@ -599,6 +931,12 @@ async function main() {
599
  await migrateTemplates(mongoDb, tursoDb, userIdMap, isDryRun);
600
  await migrateChatHistory(mongoDb, tursoDb, userIdMap, isDryRun);
601
 
 
 
 
 
 
 
602
  log("=== Migration Complete ===", "success");
603
  if (isDryRun) {
604
  log("Run without --dry-run to perform actual migration", "info");
 
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
  // =============================================================================
 
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
  // =============================================================================
 
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");
scripts/migrate-mongo-to-turso.ts DELETED
@@ -1,314 +0,0 @@
1
- /**
2
- * MongoDB to Turso Migration Script
3
- *
4
- * One-way data migration from MongoDB Atlas to Turso (SQLite).
5
- * MongoDB data remains unchanged as a read-only backup.
6
- *
7
- * Usage:
8
- * npm run migrate:mongo
9
- * npm run migrate:mongo -- --dry-run (preview only)
10
- *
11
- * Required environment variables:
12
- * MONGODB_URI - MongoDB Atlas connection string
13
- * TURSO_DATABASE_URL - Turso database URL
14
- * TURSO_AUTH_TOKEN - Turso auth token
15
- */
16
-
17
- import { MongoClient } from "mongodb";
18
- import { createClient } from "@libsql/client";
19
- import { drizzle } from "drizzle-orm/libsql";
20
- import * as schema from "../src/db/schema";
21
- import { v4 as uuidv4 } from "uuid";
22
- import * as dotenv from "dotenv";
23
-
24
- // Load environment variables
25
- dotenv.config({ path: ".env.local" });
26
-
27
- // ============================================================================
28
- // Types for MongoDB Documents
29
- // ============================================================================
30
-
31
- interface MongoUser {
32
- _id?: { toString(): string };
33
- id?: string;
34
- githubId: number;
35
- username: string;
36
- avatarUrl: string;
37
- role?: string;
38
- repositories?: string[];
39
- githubAccessToken?: string;
40
- createdAt?: Date | string;
41
- updatedAt?: Date | string;
42
- }
43
-
44
- interface MongoRepository {
45
- _id?: { toString(): string };
46
- id?: string;
47
- githubRepoId: number;
48
- name: string;
49
- owner: string;
50
- userId: string;
51
- createdAt?: Date | string;
52
- }
53
-
54
- interface MongoIssue {
55
- _id?: { toString(): string };
56
- id?: string;
57
- githubIssueId: number;
58
- number: number;
59
- title: string;
60
- body?: string;
61
- authorName: string;
62
- repoId: string;
63
- repoName: string;
64
- owner?: string;
65
- repo?: string;
66
- htmlUrl?: string;
67
- state?: string;
68
- isPR?: boolean;
69
- createdAt?: Date | string;
70
- }
71
-
72
- // ============================================================================
73
- // Utility Functions
74
- // ============================================================================
75
-
76
- function toIsoString(date: Date | string | undefined): string {
77
- if (!date) return new Date().toISOString();
78
- if (typeof date === "string") return date;
79
- return date.toISOString();
80
- }
81
-
82
- function extractId(doc: { _id?: { toString(): string }; id?: string }): string {
83
- // Prefer existing UUID id, otherwise convert ObjectId
84
- if (doc.id) return doc.id;
85
- if (doc._id) return doc._id.toString();
86
- return uuidv4();
87
- }
88
-
89
- function log(message: string, type: "info" | "success" | "error" | "warn" = "info") {
90
- const icons = { info: "ℹ️", success: "✅", error: "❌", warn: "⚠️" };
91
- console.log(`${icons[type]} ${message}`);
92
- }
93
-
94
- // ============================================================================
95
- // Migration Functions
96
- // ============================================================================
97
-
98
- async function migrateUsers(
99
- mongoDb: ReturnType<MongoClient["db"]>,
100
- tursoDb: ReturnType<typeof drizzle>,
101
- isDryRun: boolean
102
- ): Promise<Map<string, string>> {
103
- log("Migrating Users...");
104
- const userIdMap = new Map<string, string>();
105
-
106
- const users = await mongoDb.collection<MongoUser>("users").find().toArray();
107
- log(`Found ${users.length} users to migrate`);
108
-
109
- for (const mongoUser of users) {
110
- const userId = extractId(mongoUser);
111
- userIdMap.set(mongoUser._id?.toString() || mongoUser.id || "", userId);
112
-
113
- const tursoUser = {
114
- id: userId,
115
- githubId: mongoUser.githubId,
116
- username: mongoUser.username,
117
- avatarUrl: mongoUser.avatarUrl,
118
- role: mongoUser.role || null,
119
- githubAccessToken: mongoUser.githubAccessToken || null,
120
- createdAt: toIsoString(mongoUser.createdAt),
121
- updatedAt: toIsoString(mongoUser.updatedAt),
122
- };
123
-
124
- if (!isDryRun) {
125
- try {
126
- await tursoDb.insert(schema.users).values(tursoUser).onConflictDoNothing();
127
-
128
- // Migrate user repositories array to junction table
129
- if (mongoUser.repositories && mongoUser.repositories.length > 0) {
130
- for (const repoFullName of mongoUser.repositories) {
131
- await tursoDb.insert(schema.userRepositories).values({
132
- id: uuidv4(),
133
- userId: userId,
134
- repoFullName: repoFullName,
135
- addedAt: toIsoString(mongoUser.createdAt),
136
- }).onConflictDoNothing();
137
- }
138
- }
139
- } catch (error) {
140
- log(`Failed to insert user ${mongoUser.username}: ${error}`, "error");
141
- }
142
- } else {
143
- log(`[DRY RUN] Would insert user: ${mongoUser.username} (${userId})`);
144
- }
145
- }
146
-
147
- log(`Migrated ${users.length} users`, "success");
148
- return userIdMap;
149
- }
150
-
151
- async function migrateRepositories(
152
- mongoDb: ReturnType<MongoClient["db"]>,
153
- tursoDb: ReturnType<typeof drizzle>,
154
- userIdMap: Map<string, string>,
155
- isDryRun: boolean
156
- ): Promise<Map<string, string>> {
157
- log("Migrating Repositories...");
158
- const repoIdMap = new Map<string, string>();
159
-
160
- const repos = await mongoDb.collection<MongoRepository>("repositories").find().toArray();
161
- log(`Found ${repos.length} repositories to migrate`);
162
-
163
- for (const mongoRepo of repos) {
164
- const repoId = extractId(mongoRepo);
165
- repoIdMap.set(mongoRepo._id?.toString() || mongoRepo.id || "", repoId);
166
-
167
- // Map old userId to new userId
168
- const mappedUserId = userIdMap.get(mongoRepo.userId) || mongoRepo.userId;
169
-
170
- const tursoRepo = {
171
- id: repoId,
172
- githubRepoId: mongoRepo.githubRepoId,
173
- name: mongoRepo.name,
174
- owner: mongoRepo.owner,
175
- userId: mappedUserId,
176
- createdAt: toIsoString(mongoRepo.createdAt),
177
- };
178
-
179
- if (!isDryRun) {
180
- try {
181
- await tursoDb.insert(schema.repositories).values(tursoRepo).onConflictDoNothing();
182
- } catch (error) {
183
- log(`Failed to insert repo ${mongoRepo.owner}/${mongoRepo.name}: ${error}`, "error");
184
- }
185
- } else {
186
- log(`[DRY RUN] Would insert repo: ${mongoRepo.owner}/${mongoRepo.name} (${repoId})`);
187
- }
188
- }
189
-
190
- log(`Migrated ${repos.length} repositories`, "success");
191
- return repoIdMap;
192
- }
193
-
194
- async function migrateIssues(
195
- mongoDb: ReturnType<MongoClient["db"]>,
196
- tursoDb: ReturnType<typeof drizzle>,
197
- repoIdMap: Map<string, string>,
198
- isDryRun: boolean
199
- ): Promise<void> {
200
- log("Migrating Issues...");
201
-
202
- const issues = await mongoDb.collection<MongoIssue>("issues").find().toArray();
203
- log(`Found ${issues.length} issues to migrate`);
204
-
205
- let successCount = 0;
206
- let errorCount = 0;
207
-
208
- for (const mongoIssue of issues) {
209
- const issueId = extractId(mongoIssue);
210
-
211
- // Map old repoId to new repoId
212
- const mappedRepoId = repoIdMap.get(mongoIssue.repoId) || mongoIssue.repoId;
213
-
214
- const tursoIssue = {
215
- id: issueId,
216
- githubIssueId: mongoIssue.githubIssueId,
217
- number: mongoIssue.number,
218
- title: mongoIssue.title,
219
- body: mongoIssue.body || null,
220
- authorName: mongoIssue.authorName,
221
- repoId: mappedRepoId,
222
- repoName: mongoIssue.repoName,
223
- owner: mongoIssue.owner || null,
224
- repo: mongoIssue.repo || null,
225
- htmlUrl: mongoIssue.htmlUrl || null,
226
- state: mongoIssue.state || "open",
227
- isPR: mongoIssue.isPR || false,
228
- createdAt: toIsoString(mongoIssue.createdAt),
229
- };
230
-
231
- if (!isDryRun) {
232
- try {
233
- await tursoDb.insert(schema.issues).values(tursoIssue).onConflictDoNothing();
234
- successCount++;
235
- } catch (error) {
236
- log(`Failed to insert issue #${mongoIssue.number}: ${error}`, "error");
237
- errorCount++;
238
- }
239
- } else {
240
- log(`[DRY RUN] Would insert issue: #${mongoIssue.number} - ${mongoIssue.title.substring(0, 40)}...`);
241
- successCount++;
242
- }
243
- }
244
-
245
- log(`Migrated ${successCount} issues (${errorCount} errors)`, successCount > 0 ? "success" : "warn");
246
- }
247
-
248
- // ============================================================================
249
- // Main Migration Entry Point
250
- // ============================================================================
251
-
252
- async function main() {
253
- const args = process.argv.slice(2);
254
- const isDryRun = args.includes("--dry-run");
255
-
256
- if (isDryRun) {
257
- log("Running in DRY RUN mode - no data will be written", "warn");
258
- }
259
-
260
- // Validate environment variables
261
- const mongoUri = process.env.MONGODB_URI;
262
- const tursoUrl = process.env.TURSO_DATABASE_URL;
263
- const tursoToken = process.env.TURSO_AUTH_TOKEN;
264
-
265
- if (!mongoUri) {
266
- log("Missing MONGODB_URI environment variable", "error");
267
- process.exit(1);
268
- }
269
- if (!tursoUrl) {
270
- log("Missing TURSO_DATABASE_URL environment variable", "error");
271
- process.exit(1);
272
- }
273
-
274
- log("Starting MongoDB to Turso migration...");
275
- log(`MongoDB URI: ${mongoUri.replace(/\/\/[^:]+:[^@]+@/, "//***:***@")}`);
276
- log(`Turso URL: ${tursoUrl}`);
277
-
278
- // Connect to MongoDB
279
- log("Connecting to MongoDB Atlas...");
280
- const mongoClient = new MongoClient(mongoUri);
281
- await mongoClient.connect();
282
- const mongoDb = mongoClient.db();
283
- log("Connected to MongoDB", "success");
284
-
285
- // Connect to Turso
286
- log("Connecting to Turso...");
287
- const tursoClient = createClient({
288
- url: tursoUrl,
289
- authToken: tursoToken,
290
- });
291
- const tursoDb = drizzle(tursoClient, { schema });
292
- log("Connected to Turso", "success");
293
-
294
- try {
295
- // Run migrations in order (respecting foreign key relationships)
296
- const userIdMap = await migrateUsers(mongoDb, tursoDb, isDryRun);
297
- const repoIdMap = await migrateRepositories(mongoDb, tursoDb, userIdMap, isDryRun);
298
- await migrateIssues(mongoDb, tursoDb, repoIdMap, isDryRun);
299
-
300
- log("Migration completed successfully!", "success");
301
-
302
- if (isDryRun) {
303
- log("This was a dry run. Run without --dry-run to perform actual migration.", "info");
304
- }
305
- } catch (error) {
306
- log(`Migration failed: ${error}`, "error");
307
- process.exit(1);
308
- } finally {
309
- await mongoClient.close();
310
- log("Closed MongoDB connection");
311
- }
312
- }
313
-
314
- main().catch(console.error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/api/maintainer/issues/route.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Maintainer Issues Route
3
+ *
4
+ * GET /api/maintainer/issues
5
+ * Fetch issues for a maintainer with filtering and pagination.
6
+ */
7
+
8
+ import { NextRequest, NextResponse } from "next/server";
9
+ import { getCurrentUser } from "@/lib/auth";
10
+ import { getIssues, getIssuesWithTriage, IssueFilters } from "@/lib/db/queries/issues";
11
+
12
+ export async function GET(request: NextRequest) {
13
+ try {
14
+ const user = await getCurrentUser(request);
15
+ if (!user) {
16
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
17
+ }
18
+
19
+ const { searchParams } = new URL(request.url);
20
+ const page = parseInt(searchParams.get("page") || "1");
21
+ const limit = parseInt(searchParams.get("limit") || "10");
22
+ const state = searchParams.get("state") || undefined;
23
+ const repoId = searchParams.get("repoId") || undefined;
24
+ const search = searchParams.get("search") || undefined;
25
+ const withTriage = searchParams.get("withTriage") === "true";
26
+
27
+ const filters: IssueFilters = {
28
+ userId: user.id, // Filter by user's repos
29
+ state,
30
+ repoId,
31
+ search,
32
+ isPR: false, // Only fetch issues, not PRs
33
+ };
34
+
35
+ const result = withTriage
36
+ ? await getIssuesWithTriage(filters, page, limit)
37
+ : await getIssues(filters, page, limit);
38
+
39
+ return NextResponse.json(result);
40
+ } catch (error) {
41
+ console.error("GET /api/maintainer/issues error:", error);
42
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
43
+ }
44
+ }
src/app/api/maintainer/templates/route.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Maintainer Templates Route
3
+ *
4
+ * GET /api/maintainer/templates
5
+ * Fetch templates for a maintainer.
6
+ */
7
+
8
+ import { NextRequest, NextResponse } from "next/server";
9
+ import { getCurrentUser } from "@/lib/auth";
10
+ import { getTemplatesByOwnerId } from "@/lib/db/queries/templates";
11
+
12
+ export async function GET(request: NextRequest) {
13
+ try {
14
+ const user = await getCurrentUser(request);
15
+ if (!user) {
16
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
17
+ }
18
+
19
+ const templates = await getTemplatesByOwnerId(user.id);
20
+ return NextResponse.json(templates);
21
+ } catch (error) {
22
+ console.error("GET /api/maintainer/templates error:", error);
23
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
24
+ }
25
+ }
src/app/api/profile/[id]/connected-repos/route.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/db";
3
+ import { profileConnectedRepos } from "@/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+
6
+ export async function GET(
7
+ request: NextRequest,
8
+ context: { params: Promise<{ id: string }> }
9
+ ) {
10
+ try {
11
+ const { id } = await context.params;
12
+ // 'id' here corresponds to the profile's userId (since userId is PK for profiles)
13
+ const repos = await db
14
+ .select()
15
+ .from(profileConnectedRepos)
16
+ .where(eq(profileConnectedRepos.profileId, id));
17
+
18
+ return NextResponse.json(repos);
19
+ } catch (error) {
20
+ console.error("GET /api/profile/:id/connected-repos error:", error);
21
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
22
+ }
23
+ }
src/app/api/profile/[username]/featured-badges/route.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getUserBadges } from "@/lib/db/queries/gamification";
3
+
4
+ export async function GET(
5
+ request: NextRequest,
6
+ context: { params: Promise<{ username: string }> }
7
+ ) {
8
+ try {
9
+ const { username } = await context.params;
10
+ const badges = await getUserBadges(username);
11
+ // For now, just return top 5 badges as "featured"
12
+ // In the future, we could add a "featured" flag to the trophy table
13
+ const featured = badges.slice(0, 5);
14
+
15
+ return NextResponse.json(featured);
16
+ } catch (error) {
17
+ console.error("GET /api/profile/:username/featured-badges error:", error);
18
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
19
+ }
20
+ }
src/app/api/profile/[username]/repos/route.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/db";
3
+ import { repositories, users } from "@/db/schema";
4
+ import { eq, desc } from "drizzle-orm";
5
+
6
+ export async function GET(
7
+ request: NextRequest,
8
+ context: { params: Promise<{ username: string }> }
9
+ ) {
10
+ try {
11
+ const { username } = await context.params;
12
+
13
+ // Find user first to get ID
14
+ const user = await db.select().from(users).where(eq(users.username, username)).limit(1);
15
+
16
+ if (!user[0]) {
17
+ return NextResponse.json({ error: "User not found" }, { status: 404 });
18
+ }
19
+
20
+ const repos = await db
21
+ .select()
22
+ .from(repositories)
23
+ .where(eq(repositories.userId, user[0].id))
24
+ .orderBy(desc(repositories.createdAt));
25
+
26
+ return NextResponse.json(repos);
27
+ } catch (error) {
28
+ console.error("GET /api/profile/:username/repos error:", error);
29
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
30
+ }
31
+ }
src/app/api/profile/[username]/route.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getProfileByUsername } from "@/lib/db/queries/users";
3
+
4
+ export async function GET(
5
+ request: NextRequest,
6
+ context: { params: Promise<{ username: string }> }
7
+ ) {
8
+ try {
9
+ const { username } = await context.params;
10
+ const profile = await getProfileByUsername(username);
11
+ if (!profile) {
12
+ return NextResponse.json({ error: "Profile not found" }, { status: 404 });
13
+ }
14
+ return NextResponse.json(profile);
15
+ } catch (error) {
16
+ console.error("GET /api/profile/:username error:", error);
17
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
18
+ }
19
+ }
src/app/api/repositories/route.ts CHANGED
@@ -11,16 +11,26 @@ import { eq, and, desc, count, sql } from "drizzle-orm";
11
  // Python equivalent (from routes/repository.py):
12
  // repos = await db.repositories.find({"userId": user['id']}, {"_id": 0}).to_list(1000)
13
  //
 
 
 
 
14
  export async function GET(request: NextRequest) {
15
  try {
16
  const { searchParams } = new URL(request.url);
17
- const userId = searchParams.get("userId");
18
 
 
19
  if (!userId) {
20
- return NextResponse.json(
21
- { error: "userId is required" },
22
- { status: 400 }
23
- );
 
 
 
 
 
24
  }
25
 
26
  const repos = await db
 
11
  // Python equivalent (from routes/repository.py):
12
  // repos = await db.repositories.find({"userId": user['id']}, {"_id": 0}).to_list(1000)
13
  //
14
+ import { getCurrentUser } from "@/lib/auth";
15
+
16
+ // ...
17
+
18
  export async function GET(request: NextRequest) {
19
  try {
20
  const { searchParams } = new URL(request.url);
21
+ let userId = searchParams.get("userId");
22
 
23
+ // If no userId provided, try to get the current authenticated user
24
  if (!userId) {
25
+ const currentUser = await getCurrentUser(request);
26
+ if (currentUser) {
27
+ userId = currentUser.id;
28
+ } else {
29
+ return NextResponse.json(
30
+ { error: "userId is required or you must be logged in" },
31
+ { status: 401 }
32
+ );
33
+ }
34
  }
35
 
36
  const repos = await db
src/app/api/spark/badges/user/[username]/route.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getUserBadges } from "@/lib/db/queries/gamification";
3
+
4
+ export async function GET(
5
+ request: NextRequest,
6
+ context: { params: Promise<{ username: string }> }
7
+ ) {
8
+ try {
9
+ const { username } = await context.params;
10
+ const badges = await getUserBadges(username);
11
+ return NextResponse.json(badges);
12
+ } catch (error) {
13
+ console.error("GET /api/spark/badges/user/:username error:", error);
14
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
15
+ }
16
+ }
src/app/api/spark/gamification/calendar/[username]/route.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getUserCalendar } from "@/lib/db/queries/gamification";
3
+
4
+ export async function GET(
5
+ request: NextRequest,
6
+ context: { params: Promise<{ username: string }> }
7
+ ) {
8
+ try {
9
+ const { username } = await context.params;
10
+ const calendar = await getUserCalendar(username);
11
+ return NextResponse.json(calendar);
12
+ } catch (error) {
13
+ console.error("GET /api/spark/gamification/calendar/:username error:", error);
14
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
15
+ }
16
+ }
src/app/api/spark/gamification/streak/[username]/route.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getUserStreak } from "@/lib/db/queries/gamification";
3
+
4
+ export async function GET(
5
+ request: NextRequest,
6
+ context: { params: Promise<{ username: string }> }
7
+ ) {
8
+ try {
9
+ const { username } = await context.params;
10
+ const streak = await getUserStreak(username);
11
+ return NextResponse.json(streak);
12
+ } catch (error) {
13
+ console.error("GET /api/spark/gamification/streak/:username error:", error);
14
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
15
+ }
16
+ }
src/lib/db/queries/gamification.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Gamification Queries - Drizzle ORM
3
+ */
4
+
5
+ import { db } from "@/db";
6
+ import { trophies, users, issues } from "@/db/schema";
7
+ import { eq, desc, and, count, not, sql } from "drizzle-orm";
8
+
9
+ // =============================================================================
10
+ // Badges / Trophies
11
+ // =============================================================================
12
+
13
+ export async function getUserBadges(username: string) {
14
+ const user = await db.select().from(users).where(eq(users.username, username)).limit(1);
15
+ if (!user[0]) return [];
16
+
17
+ return db.select()
18
+ .from(trophies)
19
+ .where(eq(trophies.userId, user[0].id))
20
+ .orderBy(desc(trophies.awardedAt));
21
+ }
22
+
23
+ // =============================================================================
24
+ // Streak
25
+ // =============================================================================
26
+
27
+ export async function getUserStreak(username: string) {
28
+ // This is a simplified streak calculation based on issue/PR creation dates
29
+ // In a real app, you might want to track daily activity explicitly
30
+
31
+ const user = await db.select().from(users).where(eq(users.username, username)).limit(1);
32
+ if (!user[0]) return { currentStreak: 0, longestStreak: 0 };
33
+
34
+ const activity = await db.select({
35
+ createdAt: issues.createdAt
36
+ })
37
+ .from(issues)
38
+ .leftJoin(users, eq(issues.authorName, users.username)) // Assuming authorName matches username
39
+ .where(eq(users.username, username))
40
+ .orderBy(desc(issues.createdAt));
41
+
42
+ if (activity.length === 0) return { currentStreak: 0, longestStreak: 0 };
43
+
44
+ // Calculate streak logic here (simplified for now)
45
+ // For now returning mock data or simple calculation as true streak logic can be complex
46
+ // pending actual "daily activity" table
47
+
48
+ return {
49
+ currentStreak: 0,
50
+ longestStreak: 0
51
+ };
52
+ }
53
+
54
+ // =============================================================================
55
+ // Clean Calendar Data
56
+ // =============================================================================
57
+
58
+ export async function getUserCalendar(username: string) {
59
+ const user = await db.select().from(users).where(eq(users.username, username)).limit(1);
60
+ if (!user[0]) return [];
61
+
62
+ // Aggregate issues created by day
63
+ const activity = await db.select({
64
+ day: sql<string>`substr(${issues.createdAt}, 1, 10)`,
65
+ value: count()
66
+ })
67
+ .from(issues)
68
+ .leftJoin(users, eq(issues.authorName, users.username))
69
+ .where(eq(users.username, username))
70
+ .groupBy(sql`substr(${issues.createdAt}, 1, 10)`)
71
+ .orderBy(sql`day`);
72
+
73
+ return activity;
74
+ }