Spaces:
Sleeping
Sleeping
Commit ·
3eab45c
1
Parent(s): a061734
Add missing API endpoints for contributor, messaging, and RAG
Browse files- scripts/migrate-data.ts +338 -0
- scripts/migrate-mongo-to-turso.ts +0 -314
- src/app/api/maintainer/issues/route.ts +44 -0
- src/app/api/maintainer/templates/route.ts +25 -0
- src/app/api/profile/[id]/connected-repos/route.ts +23 -0
- src/app/api/profile/[username]/featured-badges/route.ts +20 -0
- src/app/api/profile/[username]/repos/route.ts +31 -0
- src/app/api/profile/[username]/route.ts +19 -0
- src/app/api/repositories/route.ts +15 -5
- src/app/api/spark/badges/user/[username]/route.ts +16 -0
- src/app/api/spark/gamification/calendar/[username]/route.ts +16 -0
- src/app/api/spark/gamification/streak/[username]/route.ts +16 -0
- src/lib/db/queries/gamification.ts +74 -0
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 |
-
|
| 18 |
|
|
|
|
| 19 |
if (!userId) {
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 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 |
+
}
|