diff --git a/scripts/migrate-data.ts b/scripts/migrate-data.ts deleted file mode 100644 index 911dbea531219dbfbf73ebb33982ec990e132d64..0000000000000000000000000000000000000000 --- a/scripts/migrate-data.ts +++ /dev/null @@ -1,949 +0,0 @@ -/** - * MongoDB to Turso Data Migration Script - * - * Migrates all data from MongoDB Atlas to Turso (SQLite). - * MongoDB remains READ-ONLY - no deletions or modifications. - * - * Usage: - * npm run migrate:data - * npm run migrate:data -- --dry-run - * - * Required env vars: - * MONGO_URL, TURSO_DATABASE_URL, TURSO_AUTH_TOKEN - */ - -import { MongoClient, ObjectId } from "mongodb"; -import { createClient } from "@libsql/client"; -import { drizzle } from "drizzle-orm/libsql"; -import { sql } from "drizzle-orm"; -import * as schema from "../src/db/schema"; -import { v4 as uuidv4 } from "uuid"; -import * as dotenv from "dotenv"; - -dotenv.config({ path: ".env.local" }); - -// ============================================================================= -// Types -// ============================================================================= - -interface MongoUser { - _id?: ObjectId; - id?: string; - githubId: number; - username: string; - avatarUrl: string; - role?: string; - repositories?: string[]; - githubAccessToken?: string; - createdAt?: Date | string; - updatedAt?: Date | string; -} - -interface MongoRepository { - _id?: ObjectId; - id?: string; - githubRepoId: number; - name: string; - owner: string; - userId: string; - createdAt?: Date | string; -} - -interface MongoIssue { - _id?: ObjectId; - id?: string; - githubIssueId: number; - number: number; - title: string; - body?: string; - authorName: string; - repoId: string; - repoName: string; - owner?: string; - repo?: string; - htmlUrl?: string; - state?: string; - isPR?: boolean; - createdAt?: Date | string; -} - -interface MongoMessage { - _id?: ObjectId; - id?: string; - senderId: string; - receiverId: string; - content: string; - read?: boolean; - timestamp?: Date | string; -} - -interface MongoProfile { - _id?: ObjectId; - user_id: string; - username: string; - avatar_url?: string; - bio?: string; - skills?: string[]; - location?: string; - website?: string; - twitter?: string; - available_for_mentoring?: boolean; - mentoring_topics?: string[]; - connected_repos?: string[]; - profile_visibility?: string; - show_email?: boolean; - github_stats?: object; - stats_updated_at?: Date | string; - created_at?: Date | string; - updated_at?: Date | string; -} - -interface MongoTriageData { - _id?: ObjectId; - id?: string; - issueId: string; - classification: string; - summary: string; - suggestedLabel: string; - sentiment: string; - analyzedAt?: Date | string; -} - -interface MongoTemplate { - _id?: ObjectId; - id?: string; - name: string; - body: string; - ownerId: string; - triggerClassification?: string; - createdAt?: Date | string; -} - -interface MongoChatMessage { - role: string; - content: string; - timestamp?: Date | string; - githubCommentId?: string; - githubCommentUrl?: string; -} - -interface MongoChatHistory { - _id?: ObjectId; - id?: string; - userId: string; - sessionId: string; - messages?: MongoChatMessage[]; - createdAt?: Date | string; -} - -interface MongoMentor { - _id?: ObjectId; - id?: string; - userId: string; - username: string; - expertiseLevel?: string; - availabilityHoursPerWeek?: number; - timezone?: string; - isActive?: boolean; - bio?: string; - avatarUrl?: string; - techStack?: string[]; - languages?: string[]; - frameworks?: string[]; - preferredTopics?: string[]; - menteeCount?: number; - sessionsCompleted?: number; - avgRating?: number; - totalRatings?: number; - maxMentees?: number; - createdAt?: Date | string; - updatedAt?: Date | string; -} - -interface MongoTrophy { - _id?: ObjectId; - id?: string; - userId: string; - username: string; - trophyType: string; - name: string; - description: string; - icon: string; - color: string; - rarity: string; - svgData?: string; - isPublic?: boolean; - shareUrl?: string; - earnedFor?: string; - milestoneValue?: number; - awardedAt?: Date | string; -} - -interface MongoIssueChat { - _id?: ObjectId; - id?: string; - issueId: string; - userId: string; - sessionId: string; - messages?: MongoChatMessage[]; - createdAt?: Date | string; - updatedAt?: Date | string; -} - -interface MongoResource { - _id?: ObjectId; - id?: string; - repoName: string; - sourceType?: string; - sourceId?: string; - resourceType: string; - title: string; - content: string; - description?: string; - language?: string; - sharedBy: string; - sharedById: string; - tags?: string[]; - saveCount?: number; - helpfulCount?: number; - createdAt?: Date | string; - updatedAt?: Date | string; -} - -// ============================================================================= -// Utilities -// ============================================================================= - -function toIsoString(date: Date | string | undefined): string { - if (!date) return new Date().toISOString(); - if (typeof date === "string") return date; - return date.toISOString(); -} - -function extractId(doc: { _id?: ObjectId; id?: string }): string { - if (doc.id) return doc.id; - if (doc._id) return doc._id.toString(); - return uuidv4(); -} - -function log(message: string, type: "info" | "success" | "error" | "warn" = "info") { - const icons = { info: "ℹ️", success: "✅", error: "❌", warn: "⚠️" }; - const time = new Date().toISOString().split("T")[1].split(".")[0]; - console.log(`[${time}] ${icons[type]} ${message}`); -} - -// ============================================================================= -// Migration Functions -// ============================================================================= - -async function migrateUsers( - mongoDb: ReturnType, - tursoDb: ReturnType, - isDryRun: boolean -): Promise> { - log("Migrating Users..."); - const userIdMap = new Map(); - const users = await mongoDb.collection("users").find().toArray(); - log(`Found ${users.length} users`); - - let success = 0, skipped = 0; - for (const mongoUser of users) { - const userId = extractId(mongoUser); - const originalId = mongoUser._id?.toString() || mongoUser.id || ""; - userIdMap.set(originalId, userId); - - if (!isDryRun) { - try { - await tursoDb.insert(schema.users).values({ - id: userId, - githubId: mongoUser.githubId, - username: mongoUser.username, - avatarUrl: mongoUser.avatarUrl, - role: mongoUser.role || null, - githubAccessToken: mongoUser.githubAccessToken || null, - createdAt: toIsoString(mongoUser.createdAt), - updatedAt: toIsoString(mongoUser.updatedAt), - }).onConflictDoNothing(); - - // Migrate user repositories array - if (mongoUser.repositories?.length) { - for (const repoName of mongoUser.repositories) { - await tursoDb.insert(schema.userRepositories).values({ - id: uuidv4(), - userId: userId, - repoFullName: repoName, - addedAt: toIsoString(mongoUser.createdAt), - }).onConflictDoNothing(); - } - } - success++; - } catch (e) { - skipped++; - } - } else { - log(`[DRY] User: ${mongoUser.username}`); - success++; - } - } - log(`Users: ${success} migrated, ${skipped} skipped`, "success"); - return userIdMap; -} - -async function migrateRepositories( - mongoDb: ReturnType, - tursoDb: ReturnType, - userIdMap: Map, - isDryRun: boolean -): Promise> { - log("Migrating Repositories..."); - const repoIdMap = new Map(); - const repos = await mongoDb.collection("repositories").find().toArray(); - log(`Found ${repos.length} repositories`); - - let success = 0, skipped = 0; - for (const repo of repos) { - const repoId = extractId(repo); - const originalId = repo._id?.toString() || repo.id || ""; - repoIdMap.set(originalId, repoId); - - const mappedUserId = userIdMap.get(repo.userId) || repo.userId; - - if (!isDryRun) { - try { - await tursoDb.insert(schema.repositories).values({ - id: repoId, - githubRepoId: repo.githubRepoId, - name: repo.name, - owner: repo.owner, - userId: mappedUserId, - createdAt: toIsoString(repo.createdAt), - }).onConflictDoNothing(); - success++; - } catch (e) { - skipped++; - } - } else { - log(`[DRY] Repo: ${repo.owner}/${repo.name}`); - success++; - } - } - log(`Repositories: ${success} migrated, ${skipped} skipped`, "success"); - return repoIdMap; -} - -async function migrateIssues( - mongoDb: ReturnType, - tursoDb: ReturnType, - repoIdMap: Map, - isDryRun: boolean -): Promise> { - log("Migrating Issues..."); - const issueIdMap = new Map(); - const issues = await mongoDb.collection("issues").find().toArray(); - log(`Found ${issues.length} issues`); - - let success = 0, skipped = 0; - for (const issue of issues) { - const issueId = extractId(issue); - const originalId = issue._id?.toString() || issue.id || ""; - issueIdMap.set(originalId, issueId); - - const mappedRepoId = repoIdMap.get(issue.repoId) || issue.repoId; - - if (!isDryRun) { - try { - await tursoDb.insert(schema.issues).values({ - id: issueId, - githubIssueId: issue.githubIssueId, - number: issue.number, - title: issue.title, - body: issue.body || null, - authorName: issue.authorName, - repoId: mappedRepoId, - repoName: issue.repoName, - owner: issue.owner || null, - repo: issue.repo || null, - htmlUrl: issue.htmlUrl || null, - state: issue.state || "open", - isPR: issue.isPR || false, - createdAt: toIsoString(issue.createdAt), - }).onConflictDoNothing(); - success++; - } catch (e) { - skipped++; - } - } else { - log(`[DRY] Issue: #${issue.number} - ${issue.title.substring(0, 30)}...`); - success++; - } - } - log(`Issues: ${success} migrated, ${skipped} skipped`, "success"); - return issueIdMap; -} - -async function migrateMessages( - mongoDb: ReturnType, - tursoDb: ReturnType, - userIdMap: Map, - isDryRun: boolean -): Promise { - log("Migrating Messages..."); - const messages = await mongoDb.collection("messages").find().toArray(); - log(`Found ${messages.length} messages`); - - let success = 0, skipped = 0; - for (const msg of messages) { - const msgId = extractId(msg); - const senderId = userIdMap.get(msg.senderId) || msg.senderId; - const receiverId = userIdMap.get(msg.receiverId) || msg.receiverId; - - if (!isDryRun) { - try { - await tursoDb.insert(schema.messages).values({ - id: msgId, - senderId: senderId, - receiverId: receiverId, - content: msg.content, - read: msg.read || false, - timestamp: toIsoString(msg.timestamp), - }).onConflictDoNothing(); - success++; - } catch (e) { - skipped++; - } - } else { - success++; - } - } - log(`Messages: ${success} migrated, ${skipped} skipped`, "success"); -} - -async function migrateProfiles( - mongoDb: ReturnType, - tursoDb: ReturnType, - userIdMap: Map, - isDryRun: boolean -): Promise { - log("Migrating Profiles..."); - const profiles = await mongoDb.collection("profiles").find().toArray(); - log(`Found ${profiles.length} profiles`); - - let success = 0, skipped = 0; - for (const profile of profiles) { - const userId = userIdMap.get(profile.user_id) || profile.user_id; - - if (!isDryRun) { - try { - await tursoDb.insert(schema.profiles).values({ - userId: userId, - username: profile.username, - avatarUrl: profile.avatar_url || null, - bio: profile.bio || null, - location: profile.location || null, - website: profile.website || null, - twitter: profile.twitter || null, - availableForMentoring: profile.available_for_mentoring || false, - profileVisibility: profile.profile_visibility || "public", - showEmail: profile.show_email || false, - githubStats: profile.github_stats ? JSON.stringify(profile.github_stats) : null, - statsUpdatedAt: profile.stats_updated_at ? toIsoString(profile.stats_updated_at) : null, - createdAt: toIsoString(profile.created_at), - updatedAt: toIsoString(profile.updated_at), - }).onConflictDoNothing(); - - // Migrate skills - if (profile.skills?.length) { - for (const skill of profile.skills) { - await tursoDb.insert(schema.profileSkills).values({ - profileId: userId, - skill: skill, - }).onConflictDoNothing(); - } - } - - // Migrate mentoring topics - if (profile.mentoring_topics?.length) { - for (const topic of profile.mentoring_topics) { - await tursoDb.insert(schema.profileMentoringTopics).values({ - profileId: userId, - topic: topic, - }).onConflictDoNothing(); - } - } - - // Migrate connected repos - if (profile.connected_repos?.length) { - for (const repo of profile.connected_repos) { - await tursoDb.insert(schema.profileConnectedRepos).values({ - profileId: userId, - repoName: repo, - }).onConflictDoNothing(); - } - } - success++; - } catch (e) { - skipped++; - } - } else { - success++; - } - } - log(`Profiles: ${success} migrated, ${skipped} skipped`, "success"); -} - -async function migrateTriageData( - mongoDb: ReturnType, - tursoDb: ReturnType, - issueIdMap: Map, - isDryRun: boolean -): Promise { - log("Migrating Triage Data..."); - const triageData = await mongoDb.collection("triageData").find().toArray(); - log(`Found ${triageData.length} triage records`); - - let success = 0, skipped = 0; - for (const triage of triageData) { - const triageId = extractId(triage); - const issueId = issueIdMap.get(triage.issueId) || triage.issueId; - - if (!isDryRun) { - try { - await tursoDb.insert(schema.triageData).values({ - id: triageId, - issueId: issueId, - classification: triage.classification, - summary: triage.summary, - suggestedLabel: triage.suggestedLabel, - sentiment: triage.sentiment, - analyzedAt: toIsoString(triage.analyzedAt), - }).onConflictDoNothing(); - success++; - } catch (e) { - skipped++; - } - } else { - success++; - } - } - log(`Triage Data: ${success} migrated, ${skipped} skipped`, "success"); -} - -async function migrateTemplates( - mongoDb: ReturnType, - tursoDb: ReturnType, - userIdMap: Map, - isDryRun: boolean -): Promise { - log("Migrating Templates..."); - const templates = await mongoDb.collection("templates").find().toArray(); - log(`Found ${templates.length} templates`); - - let success = 0, skipped = 0; - for (const template of templates) { - const templateId = extractId(template); - const ownerId = userIdMap.get(template.ownerId) || template.ownerId; - - if (!isDryRun) { - try { - await tursoDb.insert(schema.templates).values({ - id: templateId, - name: template.name, - body: template.body, - ownerId: ownerId, - triggerClassification: template.triggerClassification || null, - createdAt: toIsoString(template.createdAt), - }).onConflictDoNothing(); - success++; - } catch (e) { - skipped++; - } - } else { - success++; - } - } - log(`Templates: ${success} migrated, ${skipped} skipped`, "success"); -} - -async function migrateChatHistory( - mongoDb: ReturnType, - tursoDb: ReturnType, - userIdMap: Map, - isDryRun: boolean -): Promise { - log("Migrating Chat History..."); - const chatHistories = await mongoDb.collection("chat_history").find().toArray(); - log(`Found ${chatHistories.length} chat histories`); - - let historySuccess = 0, historySkipped = 0; - let messageSuccess = 0, messageSkipped = 0; - - for (const history of chatHistories) { - const historyId = extractId(history); - const userId = userIdMap.get(history.userId) || history.userId; - - if (!isDryRun) { - try { - // Insert chat history record - await tursoDb.insert(schema.chatHistory).values({ - id: historyId, - userId: userId, - sessionId: history.sessionId, - createdAt: toIsoString(history.createdAt), - }).onConflictDoNothing(); - historySuccess++; - - // Insert related messages - if (history.messages?.length) { - for (const msg of history.messages) { - const msgId = uuidv4(); - try { - await tursoDb.insert(schema.chatHistoryMessages).values({ - id: msgId, - chatHistoryId: historyId, - role: msg.role, - content: msg.content, - timestamp: toIsoString(msg.timestamp), - githubCommentId: msg.githubCommentId || null, - githubCommentUrl: msg.githubCommentUrl || null, - }).onConflictDoNothing(); - messageSuccess++; - } catch (e) { - messageSkipped++; - } - } - } - } catch (e) { - historySkipped++; - } - } else { - log(`[DRY] Chat History: ${historyId} with ${history.messages?.length || 0} messages`); - historySuccess++; - messageSuccess += history.messages?.length || 0; - } - } - log(`Chat History: ${historySuccess} migrated, ${historySkipped} skipped`, "success"); - log(`Chat Messages: ${messageSuccess} migrated, ${messageSkipped} skipped`, "success"); -} - -async function migrateMentors( - mongoDb: ReturnType, - tursoDb: ReturnType, - userIdMap: Map, - isDryRun: boolean -): Promise> { - log("Migrating Mentors..."); - const mentorIdMap = new Map(); - const mentors = await mongoDb.collection("mentors").find().toArray(); - log(`Found ${mentors.length} mentors`); - - let success = 0, skipped = 0; - for (const mentor of mentors) { - const mentorId = extractId(mentor); - const originalId = mentor._id?.toString() || mentor.id || ""; - mentorIdMap.set(originalId, mentorId); - - const mappedUserId = userIdMap.get(mentor.userId) || mentor.userId; - - if (!isDryRun) { - try { - await tursoDb.insert(schema.mentors).values({ - id: mentorId, - userId: mappedUserId, - username: mentor.username, - expertiseLevel: mentor.expertiseLevel || "intermediate", - availabilityHoursPerWeek: mentor.availabilityHoursPerWeek || 5, - timezone: mentor.timezone || null, - isActive: mentor.isActive ?? true, - bio: mentor.bio || null, - avatarUrl: mentor.avatarUrl || null, - menteeCount: mentor.menteeCount || 0, - sessionsCompleted: mentor.sessionsCompleted || 0, - avgRating: mentor.avgRating || 0, - totalRatings: mentor.totalRatings || 0, - maxMentees: mentor.maxMentees || 3, - createdAt: toIsoString(mentor.createdAt), - updatedAt: toIsoString(mentor.updatedAt), - }).onConflictDoNothing(); - - // Migrate tech stack - if (mentor.techStack?.length) { - for (const tech of mentor.techStack) { - await tursoDb.insert(schema.mentorTechStack).values({ - mentorId: mentorId, - tech: tech, - }).onConflictDoNothing(); - } - } - - // Migrate languages - if (mentor.languages?.length) { - for (const lang of mentor.languages) { - await tursoDb.insert(schema.mentorLanguages).values({ - mentorId: mentorId, - language: lang, - }).onConflictDoNothing(); - } - } - - // Migrate frameworks - if (mentor.frameworks?.length) { - for (const fw of mentor.frameworks) { - await tursoDb.insert(schema.mentorFrameworks).values({ - mentorId: mentorId, - framework: fw, - }).onConflictDoNothing(); - } - } - - // Migrate preferred topics - if (mentor.preferredTopics?.length) { - for (const topic of mentor.preferredTopics) { - await tursoDb.insert(schema.mentorPreferredTopics).values({ - mentorId: mentorId, - topic: topic, - }).onConflictDoNothing(); - } - } - success++; - } catch (e) { - skipped++; - } - } else { - log(`[DRY] Mentor: ${mentor.username}`); - success++; - } - } - log(`Mentors: ${success} migrated, ${skipped} skipped`, "success"); - return mentorIdMap; -} - -async function migrateTrophies( - mongoDb: ReturnType, - tursoDb: ReturnType, - userIdMap: Map, - isDryRun: boolean -): Promise { - log("Migrating Trophies..."); - const trophies = await mongoDb.collection("trophies").find().toArray(); - log(`Found ${trophies.length} trophies`); - - let success = 0, skipped = 0; - for (const trophy of trophies) { - const trophyId = extractId(trophy); - const mappedUserId = userIdMap.get(trophy.userId) || trophy.userId; - - if (!isDryRun) { - try { - await tursoDb.insert(schema.trophies).values({ - id: trophyId, - userId: mappedUserId, - username: trophy.username, - trophyType: trophy.trophyType, - name: trophy.name, - description: trophy.description, - icon: trophy.icon, - color: trophy.color, - rarity: trophy.rarity, - svgData: trophy.svgData || null, - isPublic: trophy.isPublic ?? true, - shareUrl: trophy.shareUrl || null, - earnedFor: trophy.earnedFor || null, - milestoneValue: trophy.milestoneValue || null, - awardedAt: toIsoString(trophy.awardedAt), - }).onConflictDoNothing(); - success++; - } catch (e) { - skipped++; - } - } else { - log(`[DRY] Trophy: ${trophy.name} for ${trophy.username}`); - success++; - } - } - log(`Trophies: ${success} migrated, ${skipped} skipped`, "success"); -} - -async function migrateIssueChats( - mongoDb: ReturnType, - tursoDb: ReturnType, - userIdMap: Map, - issueIdMap: Map, - isDryRun: boolean -): Promise { - log("Migrating Issue Chats..."); - const issueChats = await mongoDb.collection("issue_chats").find().toArray(); - log(`Found ${issueChats.length} issue chats`); - - let chatSuccess = 0, chatSkipped = 0; - let msgSuccess = 0, msgSkipped = 0; - - for (const chat of issueChats) { - const chatId = extractId(chat); - const mappedUserId = userIdMap.get(chat.userId) || chat.userId; - const mappedIssueId = issueIdMap.get(chat.issueId) || chat.issueId; - - if (!isDryRun) { - try { - await tursoDb.insert(schema.issueChats).values({ - id: chatId, - issueId: mappedIssueId, - userId: mappedUserId, - sessionId: chat.sessionId, - createdAt: toIsoString(chat.createdAt), - updatedAt: toIsoString(chat.updatedAt), - }).onConflictDoNothing(); - chatSuccess++; - - // Migrate messages - if (chat.messages?.length) { - for (const msg of chat.messages) { - const msgId = uuidv4(); - try { - await tursoDb.insert(schema.issueChatMessages).values({ - id: msgId, - issueChatId: chatId, - role: msg.role, - content: msg.content, - timestamp: toIsoString(msg.timestamp), - githubCommentId: msg.githubCommentId || null, - githubCommentUrl: msg.githubCommentUrl || null, - }).onConflictDoNothing(); - msgSuccess++; - } catch (e) { - msgSkipped++; - } - } - } - } catch (e) { - chatSkipped++; - } - } else { - log(`[DRY] Issue Chat: ${chatId} with ${chat.messages?.length || 0} messages`); - chatSuccess++; - msgSuccess += chat.messages?.length || 0; - } - } - log(`Issue Chats: ${chatSuccess} migrated, ${chatSkipped} skipped`, "success"); - log(`Issue Chat Messages: ${msgSuccess} migrated, ${msgSkipped} skipped`, "success"); -} - -async function migrateResources( - mongoDb: ReturnType, - tursoDb: ReturnType, - userIdMap: Map, - isDryRun: boolean -): Promise { - log("Migrating Resources..."); - const resources = await mongoDb.collection("resources").find().toArray(); - log(`Found ${resources.length} resources`); - - let success = 0, skipped = 0; - for (const resource of resources) { - const resourceId = extractId(resource); - const mappedUserId = userIdMap.get(resource.sharedById) || resource.sharedById; - - if (!isDryRun) { - try { - await tursoDb.insert(schema.resources).values({ - id: resourceId, - repoName: resource.repoName, - sourceType: resource.sourceType || "chat", - sourceId: resource.sourceId || null, - resourceType: resource.resourceType, - title: resource.title, - content: resource.content, - description: resource.description || null, - language: resource.language || null, - sharedBy: resource.sharedBy, - sharedById: mappedUserId, - saveCount: resource.saveCount || 0, - helpfulCount: resource.helpfulCount || 0, - createdAt: toIsoString(resource.createdAt), - updatedAt: toIsoString(resource.updatedAt), - }).onConflictDoNothing(); - - // Migrate tags - if (resource.tags?.length) { - for (const tag of resource.tags) { - await tursoDb.insert(schema.resourceTags).values({ - resourceId: resourceId, - tag: tag, - }).onConflictDoNothing(); - } - } - success++; - } catch (e) { - skipped++; - } - } else { - log(`[DRY] Resource: ${resource.title}`); - success++; - } - } - log(`Resources: ${success} migrated, ${skipped} skipped`, "success"); -} - -// ============================================================================= -// Main -// ============================================================================= - -async function main() { - const args = process.argv.slice(2); - const isDryRun = args.includes("--dry-run"); - - if (isDryRun) { - log("=== DRY RUN MODE ===", "warn"); - } - - const mongoUri = process.env.MONGO_URL || process.env.MONGODB_URI; - const tursoUrl = process.env.TURSO_DATABASE_URL; - const tursoToken = process.env.TURSO_AUTH_TOKEN; - - if (!mongoUri) { - log("Missing MONGO_URL or MONGODB_URI", "error"); - process.exit(1); - } - if (!tursoUrl) { - log("Missing TURSO_DATABASE_URL", "error"); - process.exit(1); - } - - log("Connecting to MongoDB..."); - const mongoClient = new MongoClient(mongoUri); - await mongoClient.connect(); - const dbName = process.env.DB_NAME || "opentriage_db"; - const mongoDb = mongoClient.db(dbName); - log(`Connected to MongoDB (${dbName})`, "success"); - - log("Connecting to Turso..."); - const tursoClient = createClient({ url: tursoUrl, authToken: tursoToken }); - const tursoDb = drizzle(tursoClient, { schema }); - log("Connected to Turso", "success"); - - try { - // Migrate in order (respecting foreign keys) - const userIdMap = await migrateUsers(mongoDb, tursoDb, isDryRun); - const repoIdMap = await migrateRepositories(mongoDb, tursoDb, userIdMap, isDryRun); - const issueIdMap = await migrateIssues(mongoDb, tursoDb, repoIdMap, isDryRun); - await migrateMessages(mongoDb, tursoDb, userIdMap, isDryRun); - await migrateProfiles(mongoDb, tursoDb, userIdMap, isDryRun); - await migrateTriageData(mongoDb, tursoDb, issueIdMap, isDryRun); - await migrateTemplates(mongoDb, tursoDb, userIdMap, isDryRun); - await migrateChatHistory(mongoDb, tursoDb, userIdMap, isDryRun); - - // New migrations for complete feature support - const mentorIdMap = await migrateMentors(mongoDb, tursoDb, userIdMap, isDryRun); - await migrateTrophies(mongoDb, tursoDb, userIdMap, isDryRun); - await migrateIssueChats(mongoDb, tursoDb, userIdMap, issueIdMap, isDryRun); - await migrateResources(mongoDb, tursoDb, userIdMap, isDryRun); - - log("=== Migration Complete ===", "success"); - if (isDryRun) { - log("Run without --dry-run to perform actual migration", "info"); - } - } finally { - await mongoClient.close(); - } -} - -main().catch(console.error); diff --git a/src/app/api/ai/chat/route.ts b/src/app/api/ai/chat/route.ts deleted file mode 100644 index 612e3826b4043b611a571b393534873183bc003c..0000000000000000000000000000000000000000 --- a/src/app/api/ai/chat/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * AI Chat Proxy Route - * - * Forwards requests to AI_ENGINE_URL/chat - */ - -import { NextRequest, NextResponse } from "next/server"; -import { chat } from "@/lib/ai-client"; -import { getCurrentUser } from "@/lib/auth"; - -export async function POST(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { message, history, context } = body; - - if (!message) { - return NextResponse.json({ error: "Message is required" }, { status: 400 }); - } - - const result = await chat(message, history, { - ...context, - userId: user.id, - username: user.username, - role: user.role, - }); - - if (!result.success) { - return NextResponse.json({ error: result.error }, { status: 502 }); - } - - return NextResponse.json(result.data); - } catch (error) { - console.error("AI Chat error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/ai/mentor-match/route.ts b/src/app/api/ai/mentor-match/route.ts deleted file mode 100644 index f5ff4e33f5e55d604fb0cdbd02b4bcf9afa33c9a..0000000000000000000000000000000000000000 --- a/src/app/api/ai/mentor-match/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * AI Mentor Match Proxy Route - * - * Forwards requests to AI_ENGINE_URL/mentor-match - */ - -import { NextRequest, NextResponse } from "next/server"; -import { findMentorMatches } from "@/lib/ai-client"; -import { getCurrentUser } from "@/lib/auth"; - -export async function POST(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { limit = 5 } = body; - - const result = await findMentorMatches(user.id, user.username, limit); - - if (!result.success) { - return NextResponse.json({ error: result.error }, { status: 502 }); - } - - return NextResponse.json(result.data); - } catch (error) { - console.error("AI Mentor Match error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/ai/rag/route.ts b/src/app/api/ai/rag/route.ts deleted file mode 100644 index 83f521a76a9920270942d7130b8babf0daa14d8a..0000000000000000000000000000000000000000 --- a/src/app/api/ai/rag/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * AI RAG Proxy Route - * - * Forwards requests to AI_ENGINE_URL/rag/chat - */ - -import { NextRequest, NextResponse } from "next/server"; -import { ragQuery } from "@/lib/ai-client"; -import { getCurrentUser } from "@/lib/auth"; - -export async function POST(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { question, repoName } = body; - - if (!question) { - return NextResponse.json({ error: "Question is required" }, { status: 400 }); - } - - const result = await ragQuery(question, repoName); - - if (!result.success) { - return NextResponse.json({ error: result.error }, { status: 502 }); - } - - return NextResponse.json(result.data); - } catch (error) { - console.error("AI RAG error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/ai/triage/route.ts b/src/app/api/ai/triage/route.ts deleted file mode 100644 index 871d7ce0394e3d3831efdcbf3d91e9bb80c60bf8..0000000000000000000000000000000000000000 --- a/src/app/api/ai/triage/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * AI Triage Proxy Route - * - * Forwards requests to AI_ENGINE_URL/triage - */ - -import { NextRequest, NextResponse } from "next/server"; -import { triageIssue } from "@/lib/ai-client"; -import { getCurrentUser } from "@/lib/auth"; - -export async function POST(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { title, body: issueBody, authorName, isPR } = body; - - if (!title) { - return NextResponse.json({ error: "Title is required" }, { status: 400 }); - } - - const result = await triageIssue({ - title, - body: issueBody, - authorName: authorName || "unknown", - isPR: isPR || false, - }); - - if (!result.success) { - return NextResponse.json({ error: result.error }, { status: 502 }); - } - - return NextResponse.json(result.data); - } catch (error) { - console.error("AI Triage error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/auth/github/callback/route.ts b/src/app/api/auth/github/callback/route.ts deleted file mode 100644 index b2c74821dcf7f37c27294f141fe5012d68397b4b..0000000000000000000000000000000000000000 --- a/src/app/api/auth/github/callback/route.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/db"; -import { users } from "@/db/schema"; -import { createJwtToken } from "@/lib/auth"; -import { generateId, now } from "@/lib/utils"; -import { eq } from "drizzle-orm"; - -const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID!; -const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET!; -const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:5173"; - -/** - * GET /api/auth/github/callback - * Handle GitHub OAuth callback and create user session. - */ -export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - const code = searchParams.get("code"); - - if (!code) { - return NextResponse.redirect(`${FRONTEND_URL}/?error=no_code`); - } - - try { - // Exchange code for access token - const tokenResponse = await fetch( - "https://github.com/login/oauth/access_token", - { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - client_id: GITHUB_CLIENT_ID, - client_secret: GITHUB_CLIENT_SECRET, - code, - }), - } - ); - - const tokenData = await tokenResponse.json(); - const accessToken = tokenData.access_token; - - if (!accessToken) { - console.error("No access token:", tokenData); - return NextResponse.redirect(`${FRONTEND_URL}/?error=no_token`); - } - - // Get user info from GitHub - const userResponse = await fetch("https://api.github.com/user", { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - - const githubUser = await userResponse.json(); - - // Check if user exists - const existingUsers = await db - .select() - .from(users) - .where(eq(users.githubId, githubUser.id)) - .limit(1); - - let userData; - - if (existingUsers.length > 0) { - // Update existing user with new GitHub token - await db - .update(users) - .set({ githubAccessToken: accessToken, updatedAt: now() }) - .where(eq(users.githubId, githubUser.id)); - - userData = { ...existingUsers[0], githubAccessToken: accessToken }; - } else { - // Create new user - const newUser = { - id: generateId(), - githubId: githubUser.id, - username: githubUser.login, - avatarUrl: githubUser.avatar_url, - role: null, - githubAccessToken: accessToken, - createdAt: now(), - updatedAt: now(), - }; - - await db.insert(users).values(newUser); - userData = newUser; - } - - // Create JWT token - const token = createJwtToken(userData.id, userData.role); - - return NextResponse.redirect(`${FRONTEND_URL}/?token=${token}`); - } catch (error) { - console.error("GitHub auth error:", error); - return NextResponse.redirect(`${FRONTEND_URL}/?error=auth_failed`); - } -} diff --git a/src/app/api/auth/github/route.ts b/src/app/api/auth/github/route.ts deleted file mode 100644 index 0287d2e1e9050be3ac9aff67bd084b0cd8f37dac..0000000000000000000000000000000000000000 --- a/src/app/api/auth/github/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID!; -const API_URL = process.env.API_URL || "http://localhost:3000"; - -/** - * GET /api/auth/github - * Redirect to GitHub OAuth authorization page. - */ -export async function GET(request: NextRequest) { - const callbackUrl = `${API_URL}/api/auth/github/callback`; - const githubUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&redirect_uri=${callbackUrl}&scope=user:email,repo`; - - return NextResponse.redirect(githubUrl); -} diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts deleted file mode 100644 index 2934f6fe5e231aba2042a3ed54c5142b945fb1dc..0000000000000000000000000000000000000000 --- a/src/app/api/auth/me/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { requireAuth } from "@/lib/auth"; - -/** - * GET /api/auth/me - * Get current authenticated user information. - */ -export async function GET(request: NextRequest) { - const { user, error } = await requireAuth(request); - - if (error) { - return error; - } - - return NextResponse.json({ - id: user!.id, - username: user!.username, - avatarUrl: user!.avatarUrl, - role: user!.role, - githubId: user!.githubId, - }); -} diff --git a/src/app/api/auth/select-role/route.ts b/src/app/api/auth/select-role/route.ts deleted file mode 100644 index 5c8c0d42c60de54f72191f6c569a6a9af5a2241f..0000000000000000000000000000000000000000 --- a/src/app/api/auth/select-role/route.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Role Selection Route - * - * POST /api/auth/select-role - * Allows authenticated users to select their role (MAINTAINER or CONTRIBUTOR) - */ - -import { NextRequest, NextResponse } from "next/server"; -import { requireAuth, createJwtToken } from "@/lib/auth"; -import { updateUserRole } from "@/lib/db/queries/users"; - -export async function POST(request: NextRequest) { - const { user, error } = await requireAuth(request); - - if (error) { - return error; - } - - try { - const body = await request.json(); - const { role } = body; - - // Validate role - if (!role || !["MAINTAINER", "CONTRIBUTOR"].includes(role.toUpperCase())) { - return NextResponse.json( - { error: "Invalid role. Must be MAINTAINER or CONTRIBUTOR" }, - { status: 400 } - ); - } - - const normalizedRole = role.toUpperCase(); - - // Update user role in database - await updateUserRole(user!.id, normalizedRole); - - // Generate new token with updated role - const newToken = createJwtToken(user!.id, normalizedRole); - - return NextResponse.json({ - success: true, - role: normalizedRole, - token: newToken, - }); - } catch (error) { - console.error("Role selection error:", error); - return NextResponse.json( - { error: "Failed to update role" }, - { status: 500 } - ); - } -} diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts deleted file mode 100644 index 0abbd778f6786b72c3ea10136f7e056d3df23da9..0000000000000000000000000000000000000000 --- a/src/app/api/chat/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Chat Route - * - * POST /api/chat - * Proxy to /api/ai/chat for convenience - */ - -import { NextRequest, NextResponse } from "next/server"; -import { chat } from "@/lib/ai-client"; -import { getCurrentUser } from "@/lib/auth"; - -export async function POST(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { message, sessionId, history, context } = body; - - if (!message) { - return NextResponse.json({ error: "Message is required" }, { status: 400 }); - } - - const result = await chat(message, history, { - ...context, - sessionId, - userId: user.id, - username: user.username, - role: user.role, - }); - - if (!result.success) { - return NextResponse.json({ error: result.error }, { status: 502 }); - } - - return NextResponse.json(result.data); - } catch (error) { - console.error("Chat error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/contributor/claim-activity/[issueId]/route.ts b/src/app/api/contributor/claim-activity/[issueId]/route.ts deleted file mode 100644 index 72292517eabbe798684df8e1ca577187c08f0792..0000000000000000000000000000000000000000 --- a/src/app/api/contributor/claim-activity/[issueId]/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Claim Activity Route - * - * POST /api/contributor/claim-activity/[issueId] - * Update activity timestamp for a claimed issue - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ issueId: string }> } -) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { issueId } = await params; - - // TODO: Implement with claimed_issues table - return NextResponse.json({ - message: "Activity updated successfully (stub)", - issueId, - }); - } catch (error) { - console.error("Claim activity error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/contributor/claim-issue/[issueId]/route.ts b/src/app/api/contributor/claim-issue/[issueId]/route.ts deleted file mode 100644 index d209a85c0d95c9d105db52cd2c29212dcaf1e71c..0000000000000000000000000000000000000000 --- a/src/app/api/contributor/claim-issue/[issueId]/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Unclaim Issue Route - * - * DELETE /api/contributor/claim-issue/[issueId] - * Unclaim a previously claimed issue - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; - -export async function DELETE( - request: NextRequest, - { params }: { params: Promise<{ issueId: string }> } -) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { issueId } = await params; - - // TODO: Implement full unclaiming logic with claimed_issues table - return NextResponse.json({ - message: "Issue unclaimed successfully (stub)", - issueId, - }); - } catch (error) { - console.error("Unclaim issue error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/contributor/claim-issue/route.ts b/src/app/api/contributor/claim-issue/route.ts deleted file mode 100644 index fef25957fd7548c5a23284b0127c1a5e87218ec8..0000000000000000000000000000000000000000 --- a/src/app/api/contributor/claim-issue/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Issue Claiming Routes - * - * POST /api/contributor/claim-issue - * Claim an issue to work on - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; - -// Note: Full implementation requires adding claimed_issues table to schema -// This is a stub that prevents 404 errors - -export async function POST(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { issueId } = body; - - if (!issueId) { - return NextResponse.json({ error: "issueId is required" }, { status: 400 }); - } - - // TODO: Implement full claiming logic with claimed_issues table - return NextResponse.json({ - message: "Issue claim registered (stub)", - issueId, - claimedAt: new Date().toISOString(), - }); - } catch (error) { - console.error("Claim issue error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/contributor/dashboard-summary/route.ts b/src/app/api/contributor/dashboard-summary/route.ts deleted file mode 100644 index 4581539d012dbcd1a97059025a82d3d7369266fb..0000000000000000000000000000000000000000 --- a/src/app/api/contributor/dashboard-summary/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Contributor Dashboard Summary Route - * - * GET /api/contributor/dashboard-summary - * Get dashboard statistics for the contributor - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getIssuesWithTriage } from "@/lib/db/queries/issues"; -import { getContributorRepositories } from "@/lib/db/queries/repositories"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - // Get all contributor's issues (no pagination for stats) - const [repos, issuesData] = await Promise.all([ - getContributorRepositories(user.id, user.username), - getIssuesWithTriage({ authorName: user.username }, 1, 1000), // Get all for accurate stats - ]); - - const allIssues = issuesData.issues; - const myIssues = allIssues.filter(i => !i.isPR); - const myPRs = allIssues.filter(i => i.isPR); - - return NextResponse.json({ - totalContributions: allIssues.length, - totalPRs: myPRs.length, - openPRs: myPRs.filter(i => i.state === "open").length, - mergedPRs: myPRs.filter(i => i.state === "closed").length, - totalIssues: myIssues.length, - openIssues: myIssues.filter(i => i.state === "open").length, - closedIssues: myIssues.filter(i => i.state === "closed").length, - repositoriesContributed: repos.length, - }); - } catch (error) { - console.error("Contributor dashboard-summary error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/contributor/my-claimed-issues/route.ts b/src/app/api/contributor/my-claimed-issues/route.ts deleted file mode 100644 index a5ace9d16833d2693b6dc1f04f96ed965df0d3b8..0000000000000000000000000000000000000000 --- a/src/app/api/contributor/my-claimed-issues/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * My Claimed Issues Route - * - * GET /api/contributor/my-claimed-issues - * Get all issues claimed by the current user - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - // TODO: Implement with claimed_issues table - return NextResponse.json({ - claims: [], - count: 0, - }); - } catch (error) { - console.error("My claimed issues error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/contributor/my-issues/route.ts b/src/app/api/contributor/my-issues/route.ts deleted file mode 100644 index f1754a4a718902f8d49d072a9631ab0f2a506ee8..0000000000000000000000000000000000000000 --- a/src/app/api/contributor/my-issues/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Contributor My Issues Route - * - * GET /api/contributor/my-issues - * Get paginated list of contributor's issues and PRs - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getIssuesWithTriage } from "@/lib/db/queries/issues"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const page = parseInt(searchParams.get("page") || "1"); - const limit = parseInt(searchParams.get("limit") || "10"); - - const issuesData = await getIssuesWithTriage({ authorName: user.username }, page, limit); - - return NextResponse.json({ - items: issuesData.issues, - total: issuesData.total, - page: issuesData.page, - pages: issuesData.totalPages, - limit: issuesData.limit, - }); - } catch (error) { - console.error("Contributor my-issues error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/contributor/route.ts b/src/app/api/contributor/route.ts deleted file mode 100644 index e7a520bb2c1c36dced3ffac344979a0ab4964974..0000000000000000000000000000000000000000 --- a/src/app/api/contributor/route.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Contributor Dashboard Route - * - * Get dashboard stats and issues for contributors. - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getIssuesWithTriage } from "@/lib/db/queries/issues"; -import { getContributorRepositories } from "@/lib/db/queries/repositories"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const page = parseInt(searchParams.get("page") || "1"); - const limit = parseInt(searchParams.get("limit") || "10"); - - // Get contributor's issues - const [repos, issuesData] = await Promise.all([ - getContributorRepositories(user.id, user.username), - getIssuesWithTriage({ authorName: user.username }, page, limit), - ]); - - // Calculate stats - const myIssues = issuesData.issues.filter(i => !i.isPR); - const myPRs = issuesData.issues.filter(i => i.isPR); - - return NextResponse.json({ - stats: { - totalIssues: myIssues.length, - totalPRs: myPRs.length, - openIssues: myIssues.filter(i => i.state === "open").length, - openPRs: myPRs.filter(i => i.state === "open").length, - repositoriesContributed: repos.length, - }, - repositories: repos, - issues: issuesData.issues, - pagination: { - page: issuesData.page, - limit: issuesData.limit, - total: issuesData.total, - totalPages: issuesData.totalPages, - } - }); - } catch (error) { - console.error("Contributor dashboard error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/issues/[id]/messages/route.ts b/src/app/api/issues/[id]/messages/route.ts deleted file mode 100644 index 2048813be04f8d6f4225078a99302eaf0665cc0c..0000000000000000000000000000000000000000 --- a/src/app/api/issues/[id]/messages/route.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/db"; -import { chatMessages, issues } from "@/db/schema"; -import { requireAuth } from "@/lib/auth"; -import { generateId, now } from "@/lib/utils"; -import { eq, desc, gt, and } from "drizzle-orm"; - -type RouteContext = { - params: Promise<{ id: string }>; -}; - -/** - * GET /api/issues/[id]/messages - * Get chat messages for an issue. - */ -export async function GET( - request: NextRequest, - context: RouteContext -) { - const { id: issueId } = await context.params; - const { searchParams } = new URL(request.url); - const afterId = searchParams.get("afterId"); - const limit = parseInt(searchParams.get("limit") || "50", 10); - - try { - // Verify issue exists - const issueRecords = await db - .select() - .from(issues) - .where(eq(issues.id, issueId)) - .limit(1); - - if (issueRecords.length === 0) { - return NextResponse.json( - { error: "Issue not found" }, - { status: 404 } - ); - } - - // Build query for messages - // Note: We use sessionId to associate messages with issues - // In a proper implementation, you'd have a session per issue - let messagesQuery = db - .select() - .from(chatMessages) - .where(eq(chatMessages.sessionId, issueId)) - .orderBy(desc(chatMessages.timestamp)) - .limit(limit); - - const messages = await messagesQuery; - - // Reverse to show oldest first - messages.reverse(); - - return NextResponse.json({ - messages, - count: messages.length, - issueId, - }); - } catch (error) { - console.error("GET /api/issues/[id]/messages error:", error); - return NextResponse.json( - { error: "Failed to fetch messages" }, - { status: 500 } - ); - } -} - -/** - * POST /api/issues/[id]/messages - * Send a chat message for an issue. - * - * Request body: { content: string, messageType?: string } - */ -export async function POST( - request: NextRequest, - context: RouteContext -) { - const { user, error } = await requireAuth(request); - if (error) return error; - - const { id: issueId } = await context.params; - - try { - const body = await request.json(); - const { content, messageType = "text" } = body; - - if (!content || typeof content !== "string") { - return NextResponse.json( - { error: "content is required" }, - { status: 400 } - ); - } - - // Verify issue exists - const issueRecords = await db - .select() - .from(issues) - .where(eq(issues.id, issueId)) - .limit(1); - - if (issueRecords.length === 0) { - return NextResponse.json( - { error: "Issue not found" }, - { status: 404 } - ); - } - - // Create the message - const message = { - id: generateId(), - sessionId: issueId, // Using issueId as sessionId for issue-based chats - senderId: user!.id, - senderUsername: user!.username, - isMentor: false, - content, - messageType, - language: null, - isAiGenerated: false, - containsResource: false, - extractedResourceId: null, - timestamp: now(), - editedAt: null, - }; - - await db.insert(chatMessages).values(message); - - return NextResponse.json({ - message: "Message sent", - data: message, - }, { status: 201 }); - } catch (error) { - console.error("POST /api/issues/[id]/messages error:", error); - return NextResponse.json( - { error: "Failed to send message" }, - { status: 500 } - ); - } -} diff --git a/src/app/api/issues/route.ts b/src/app/api/issues/route.ts deleted file mode 100644 index ccc05d952190f8fb789fa81afce5ca015cd7b17c..0000000000000000000000000000000000000000 --- a/src/app/api/issues/route.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/db"; -import { issues, triageData, repositories } from "@/db/schema"; -import { generateId, now } from "@/lib/utils"; -import { eq, and, desc } from "drizzle-orm"; - -// ============================================================================= -// GET /api/issues - Get issues for a repository -// ============================================================================= -// -// Python equivalent (from routes/repository.py): -// repos = await db.issues.find({"repoId": repo_id}, {"_id": 0}).to_list(1000) -// -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const repoId = searchParams.get("repoId"); - const state = searchParams.get("state"); // open, closed, all - const isPR = searchParams.get("isPR"); // true, false - - // Build query conditions - const conditions = []; - if (repoId) { - conditions.push(eq(issues.repoId, repoId)); - } - if (state && state !== "all") { - conditions.push(eq(issues.state, state)); - } - if (isPR !== null && isPR !== undefined) { - conditions.push(eq(issues.isPR, isPR === "true")); - } - - // Execute query with Drizzle - const result = await db - .select() - .from(issues) - .where(conditions.length > 0 ? and(...conditions) : undefined) - .orderBy(desc(issues.createdAt)) - .limit(100); - - return NextResponse.json(result, { status: 200 }); - } catch (error) { - console.error("GET /api/issues error:", error); - return NextResponse.json( - { error: "Failed to fetch issues" }, - { status: 500 } - ); - } -} - -// ============================================================================= -// POST /api/issues - Create a new issue -// ============================================================================= -// -// Python equivalent (from routes/repository.py): -// issue = Issue( -// githubIssueId=gh_issue['id'], -// number=gh_issue['number'], -// title=gh_issue['title'], -// body=gh_issue.get('body') or '', -// authorName=gh_issue['user']['login'], -// repoId=repository.id, -// repoName=repository.name, -// ... -// ) -// issue_dict = issue.model_dump() -// issue_dict['createdAt'] = issue_dict['createdAt'].isoformat() -// await db.issues.insert_one(issue_dict) -// -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - - // Validate required fields - const { githubIssueId, number, title, authorName, repoId, repoName } = body; - if (!githubIssueId || !number || !title || !authorName || !repoId || !repoName) { - return NextResponse.json( - { error: "Missing required fields" }, - { status: 400 } - ); - } - - // Check if repository exists - const repo = await db - .select() - .from(repositories) - .where(eq(repositories.id, repoId)) - .limit(1); - - if (repo.length === 0) { - return NextResponse.json( - { error: "Repository not found" }, - { status: 404 } - ); - } - - // Check if issue already exists - const existingIssue = await db - .select() - .from(issues) - .where(eq(issues.githubIssueId, githubIssueId)) - .limit(1); - - if (existingIssue.length > 0) { - return NextResponse.json( - { error: "Issue already exists", issue: existingIssue[0] }, - { status: 409 } - ); - } - - // Create new issue - const newIssue = { - id: generateId(), - githubIssueId, - number, - title, - body: body.body || "", - authorName, - repoId, - repoName, - owner: body.owner || "", - repo: body.repo || "", - htmlUrl: body.htmlUrl || "", - state: body.state || "open", - isPR: body.isPR || false, - createdAt: now(), - }; - - await db.insert(issues).values(newIssue); - - return NextResponse.json( - { message: "Issue created", issue: newIssue }, - { status: 201 } - ); - } catch (error) { - console.error("POST /api/issues error:", error); - return NextResponse.json( - { error: "Failed to create issue" }, - { status: 500 } - ); - } -} diff --git a/src/app/api/maintainer/dashboard-summary/route.ts b/src/app/api/maintainer/dashboard-summary/route.ts deleted file mode 100644 index a6a6c5ecf8f3fed814da89c92e1582a681c31622..0000000000000000000000000000000000000000 --- a/src/app/api/maintainer/dashboard-summary/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Maintainer Dashboard Summary Route - * - * GET /api/maintainer/dashboard-summary - * Get dashboard statistics for maintainers - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getDashboardStats } from "@/lib/db/queries/issues"; -import { getMaintainerRepositories } from "@/lib/db/queries/repositories"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - if (user.role !== "MAINTAINER" && user.role !== "maintainer") { - return NextResponse.json({ error: "Maintainer access required" }, { status: 403 }); - } - - // Get dashboard stats - const [stats, repos] = await Promise.all([ - getDashboardStats(user.id), - getMaintainerRepositories(user.id), - ]); - - return NextResponse.json({ - ...stats, - repositoriesCount: repos.length, - repositories: repos, - }); - } catch (error) { - console.error("Maintainer dashboard-summary error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/maintainer/issues/route.ts b/src/app/api/maintainer/issues/route.ts deleted file mode 100644 index e199b2229df02b5d24b3447dda6a38f667483989..0000000000000000000000000000000000000000 --- a/src/app/api/maintainer/issues/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Maintainer Issues Route - * - * GET /api/maintainer/issues - * Fetch issues for a maintainer with filtering and pagination. - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getIssues, getIssuesWithTriage, IssueFilters } from "@/lib/db/queries/issues"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const page = parseInt(searchParams.get("page") || "1"); - const limit = parseInt(searchParams.get("limit") || "10"); - const state = searchParams.get("state") || undefined; - const repoId = searchParams.get("repoId") || undefined; - const search = searchParams.get("search") || undefined; - const withTriage = searchParams.get("withTriage") === "true"; - - const filters: IssueFilters = { - userId: user.id, // Filter by user's repos - state, - repoId, - search, - isPR: false, // Only fetch issues, not PRs - }; - - const result = withTriage - ? await getIssuesWithTriage(filters, page, limit) - : await getIssues(filters, page, limit); - - return NextResponse.json(result); - } catch (error) { - console.error("GET /api/maintainer/issues error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/maintainer/route.ts b/src/app/api/maintainer/route.ts deleted file mode 100644 index b3f3979a1b568d25d82708f863955f9393a86a24..0000000000000000000000000000000000000000 --- a/src/app/api/maintainer/route.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Maintainer Dashboard Route - * - * Get dashboard stats, issues, and templates for maintainers. - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getDashboardStats, getIssuesWithTriage } from "@/lib/db/queries/issues"; -import { getMaintainerRepositories, getRepositoryStats } from "@/lib/db/queries/repositories"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - if (user.role !== "MAINTAINER" && user.role !== "maintainer") { - return NextResponse.json({ error: "Maintainer access required" }, { status: 403 }); - } - - const { searchParams } = new URL(request.url); - const page = parseInt(searchParams.get("page") || "1"); - const limit = parseInt(searchParams.get("limit") || "10"); - - // Get dashboard data - const [stats, repos, issuesData] = await Promise.all([ - getDashboardStats(user.id), - getMaintainerRepositories(user.id), - getIssuesWithTriage({ userId: user.id, state: "open" }, page, limit), - ]); - - return NextResponse.json({ - stats, - repositories: repos, - issues: issuesData.issues, - pagination: { - page: issuesData.page, - limit: issuesData.limit, - total: issuesData.total, - totalPages: issuesData.totalPages, - } - }); - } catch (error) { - console.error("Maintainer dashboard error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/maintainer/templates/route.ts b/src/app/api/maintainer/templates/route.ts deleted file mode 100644 index a82e3d83affd03dabef3b5c795b32232960c8f77..0000000000000000000000000000000000000000 --- a/src/app/api/maintainer/templates/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Maintainer Templates Route - * - * GET /api/maintainer/templates - * Fetch templates for a maintainer. - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getTemplatesByOwnerId } from "@/lib/db/queries/templates"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const templates = await getTemplatesByOwnerId(user.id); - return NextResponse.json(templates); - } catch (error) { - console.error("GET /api/maintainer/templates error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/messages/route.ts b/src/app/api/messages/route.ts deleted file mode 100644 index 4d6a0ce3970adc80a7934672dac3ba9b352c8ef4..0000000000000000000000000000000000000000 --- a/src/app/api/messages/route.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Messages Route - * - * Get conversations and send messages. - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getConversations, getChatHistory, sendMessage, markMessagesAsRead, getUnreadCount } from "@/lib/db/queries/messages"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const otherUserId = searchParams.get("with"); - - if (otherUserId) { - // Get chat history with specific user - const history = await getChatHistory(user.id, otherUserId); - await markMessagesAsRead(user.id, otherUserId); - return NextResponse.json({ messages: history }); - } else { - // Get all conversations - const conversations = await getConversations(user.id); - const unreadCount = await getUnreadCount(user.id); - return NextResponse.json({ conversations, unreadCount }); - } - } catch (error) { - console.error("Messages fetch error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} - -export async function POST(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { receiverId, content } = body; - - if (!receiverId || !content) { - return NextResponse.json({ error: "receiverId and content are required" }, { status: 400 }); - } - - const message = await sendMessage({ - senderId: user.id, - receiverId, - content, - }); - - return NextResponse.json(message); - } catch (error) { - console.error("Message send error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/messaging/conversations/route.ts b/src/app/api/messaging/conversations/route.ts deleted file mode 100644 index 93f80161dcfc3331abee5ea6613da12ea37caa75..0000000000000000000000000000000000000000 --- a/src/app/api/messaging/conversations/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Messaging Conversations Route - * - * GET /api/messaging/conversations - * Get list of all conversations for the current user - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getConversations } from "@/lib/db/queries/messages"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const conversations = await getConversations(user.id); - - // Format to match frontend expectations - return NextResponse.json({ - conversations: conversations.map(c => ({ - user_id: c.partnerId, - username: c.partnerUsername, - avatar_url: c.partnerAvatar, - last_message: c.lastMessage?.content?.substring(0, 50) || "", - last_timestamp: c.lastMessage?.timestamp || null, - unread_count: c.unreadCount - })) - }); - } catch (error) { - console.error("Conversations error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/messaging/history/[userId]/route.ts b/src/app/api/messaging/history/[userId]/route.ts deleted file mode 100644 index b1a18d7468f2abf8311b90ebf7c0a81b27dc7397..0000000000000000000000000000000000000000 --- a/src/app/api/messaging/history/[userId]/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Messaging History Route - * - * GET /api/messaging/history/[userId] - * Get chat history with a specific user - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getChatHistory, markMessagesAsRead } from "@/lib/db/queries/messages"; - -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ userId: string }> } -) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { userId: otherUserId } = await params; - - // Get chat history - const history = await getChatHistory(user.id, otherUserId); - - // Mark messages as read - await markMessagesAsRead(user.id, otherUserId); - - return NextResponse.json(history); - } catch (error) { - console.error("Chat history error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/messaging/mark-read/[userId]/route.ts b/src/app/api/messaging/mark-read/[userId]/route.ts deleted file mode 100644 index 9bf3dc6b54df071b4f8ca1b83fb21c83356fbe5b..0000000000000000000000000000000000000000 --- a/src/app/api/messaging/mark-read/[userId]/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Messaging Mark Read Route - * - * POST /api/messaging/mark-read/[userId] - * Mark all messages from a user as read - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { markMessagesAsRead } from "@/lib/db/queries/messages"; - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ userId: string }> } -) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { userId: otherUserId } = await params; - - await markMessagesAsRead(user.id, otherUserId); - - return NextResponse.json({ success: true, message: "Messages marked as read" }); - } catch (error) { - console.error("Mark read error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/messaging/poll/[userId]/route.ts b/src/app/api/messaging/poll/[userId]/route.ts deleted file mode 100644 index 670acec86114932483487cb736425a49bb2f7915..0000000000000000000000000000000000000000 --- a/src/app/api/messaging/poll/[userId]/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Messaging Poll Route - * - * GET /api/messaging/poll/[userId] - * Poll for new messages from a specific user - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { pollNewMessages } from "@/lib/db/queries/messages"; - -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ userId: string }> } -) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { userId: otherUserId } = await params; - const { searchParams } = new URL(request.url); - const lastMessageId = searchParams.get("last_message_id") || undefined; - - const newMessages = await pollNewMessages(user.id, otherUserId, lastMessageId); - - return NextResponse.json(newMessages); - } catch (error) { - console.error("Poll messages error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/messaging/route.ts b/src/app/api/messaging/route.ts deleted file mode 100644 index d1b34edc0ab9e6f91b85c51fa27ef7fbeda26a31..0000000000000000000000000000000000000000 --- a/src/app/api/messaging/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Messaging Routes - * - * Full messaging API for the frontend - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { - getConversations, - getChatHistory, - sendMessage, - markMessagesAsRead, - getUnreadCount, - pollNewMessages -} from "@/lib/db/queries/messages"; - -// GET /api/messaging - Get conversations list -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const conversations = await getConversations(user.id); - const unreadCount = await getUnreadCount(user.id); - - return NextResponse.json({ conversations, unreadCount }); - } catch (error) { - console.error("Messaging error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/messaging/send/route.ts b/src/app/api/messaging/send/route.ts deleted file mode 100644 index be3e1a2989ebb35a040e932a8145d99ecff9fed3..0000000000000000000000000000000000000000 --- a/src/app/api/messaging/send/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Messaging Send Route - * - * POST /api/messaging/send - * Send a message to another user - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { sendMessage } from "@/lib/db/queries/messages"; - -export async function POST(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { receiver_id, content } = body; - - if (!receiver_id || !content) { - return NextResponse.json( - { error: "receiver_id and content are required" }, - { status: 400 } - ); - } - - const message = await sendMessage({ - senderId: user.id, - receiverId: receiver_id, - content, - }); - - return NextResponse.json(message); - } catch (error) { - console.error("Send message error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/messaging/unread-count/route.ts b/src/app/api/messaging/unread-count/route.ts deleted file mode 100644 index b8ec31ddca2f5e78248dd8de11b9ed51469eb975..0000000000000000000000000000000000000000 --- a/src/app/api/messaging/unread-count/route.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Messaging Unread Count Route - * - * GET /api/messaging/unread-count - * Get the count of unread messages for the authenticated user - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getUnreadCount } from "@/lib/db/queries/messages"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const count = await getUnreadCount(user.id); - - return NextResponse.json({ count }); - } catch (error) { - console.error("Unread count error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/profile/[id]/connected-repos/route.ts b/src/app/api/profile/[id]/connected-repos/route.ts deleted file mode 100644 index 647cee52cf102196f07c045018f2bc16e6b0a6d9..0000000000000000000000000000000000000000 --- a/src/app/api/profile/[id]/connected-repos/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/db"; -import { profileConnectedRepos } from "@/db/schema"; -import { eq } from "drizzle-orm"; - -export async function GET( - request: NextRequest, - context: { params: Promise<{ id: string }> } -) { - try { - const { id } = await context.params; - // 'id' here corresponds to the profile's userId (since userId is PK for profiles) - const repos = await db - .select() - .from(profileConnectedRepos) - .where(eq(profileConnectedRepos.profileId, id)); - - return NextResponse.json(repos); - } catch (error) { - console.error("GET /api/profile/:id/connected-repos error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/profile/[username]/featured-badges/route.ts b/src/app/api/profile/[username]/featured-badges/route.ts deleted file mode 100644 index 0f3ec2e38b85fc6bbab126d4b7725b7dbfc055ff..0000000000000000000000000000000000000000 --- a/src/app/api/profile/[username]/featured-badges/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getUserBadges } from "@/lib/db/queries/gamification"; - -export async function GET( - request: NextRequest, - context: { params: Promise<{ username: string }> } -) { - try { - const { username } = await context.params; - const badges = await getUserBadges(username); - // For now, just return top 5 badges as "featured" - // In the future, we could add a "featured" flag to the trophy table - const featured = badges.slice(0, 5); - - return NextResponse.json(featured); - } catch (error) { - console.error("GET /api/profile/:username/featured-badges error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/profile/[username]/repos/route.ts b/src/app/api/profile/[username]/repos/route.ts deleted file mode 100644 index 1e973bfd1f646aa62a3d3d8297d57be82881dd80..0000000000000000000000000000000000000000 --- a/src/app/api/profile/[username]/repos/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/db"; -import { repositories, users } from "@/db/schema"; -import { eq, desc } from "drizzle-orm"; - -export async function GET( - request: NextRequest, - context: { params: Promise<{ username: string }> } -) { - try { - const { username } = await context.params; - - // Find user first to get ID - const user = await db.select().from(users).where(eq(users.username, username)).limit(1); - - if (!user[0]) { - return NextResponse.json({ error: "User not found" }, { status: 404 }); - } - - const repos = await db - .select() - .from(repositories) - .where(eq(repositories.userId, user[0].id)) - .orderBy(desc(repositories.createdAt)); - - return NextResponse.json(repos); - } catch (error) { - console.error("GET /api/profile/:username/repos error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/profile/[username]/route.ts b/src/app/api/profile/[username]/route.ts deleted file mode 100644 index c4902742eb593549087607e8a71259d0631eeec1..0000000000000000000000000000000000000000 --- a/src/app/api/profile/[username]/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getProfileByUsername } from "@/lib/db/queries/users"; - -export async function GET( - request: NextRequest, - context: { params: Promise<{ username: string }> } -) { - try { - const { username } = await context.params; - const profile = await getProfileByUsername(username); - if (!profile) { - return NextResponse.json({ error: "Profile not found" }, { status: 404 }); - } - return NextResponse.json(profile); - } catch (error) { - console.error("GET /api/profile/:username error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts deleted file mode 100644 index 64f7ba063bafa52a58bd891b3ab720fe9344921d..0000000000000000000000000000000000000000 --- a/src/app/api/profile/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Profile Route - * - * Get and update user profiles. - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { getProfileByUsername, createOrUpdateProfile } from "@/lib/db/queries/users"; - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const username = searchParams.get("username"); - - if (!username) { - return NextResponse.json({ error: "Username is required" }, { status: 400 }); - } - - const profile = await getProfileByUsername(username); - if (!profile) { - return NextResponse.json({ error: "Profile not found" }, { status: 404 }); - } - - return NextResponse.json(profile); - } catch (error) { - console.error("Profile fetch error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} - -export async function PUT(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { bio, location, website, twitter, skills, availableForMentoring, mentoringTopics } = body; - - const profile = await createOrUpdateProfile(user.id, { - username: user.username, - avatarUrl: user.avatarUrl, - bio, - location, - website, - twitter, - skills, - availableForMentoring, - mentoringTopics, - }); - - return NextResponse.json(profile); - } catch (error) { - console.error("Profile update error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/rag/chat/route.ts b/src/app/api/rag/chat/route.ts deleted file mode 100644 index 30aa7f4581df10a6d1be954dd83e5d0f4f27b82f..0000000000000000000000000000000000000000 --- a/src/app/api/rag/chat/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * RAG Chat Route - * - * POST /api/rag/chat - * Answer questions using RAG (Retrieval-Augmented Generation) - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { ragChat } from "@/lib/ai-client"; - -export async function POST(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { question, repo_name, top_k } = body; - - if (!question) { - return NextResponse.json({ error: "question is required" }, { status: 400 }); - } - - const result = await ragChat(question, repo_name, top_k || 5); - - if (!result.success) { - return NextResponse.json({ error: result.error }, { status: 502 }); - } - - return NextResponse.json(result.data); - } catch (error) { - console.error("RAG chat error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/rag/index/route.ts b/src/app/api/rag/index/route.ts deleted file mode 100644 index 7260056439ef199db9a8ba844461358c842f6fa7..0000000000000000000000000000000000000000 --- a/src/app/api/rag/index/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * RAG Index Route - * - * POST /api/rag/index - * Index a repository for RAG search - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { ragIndex } from "@/lib/ai-client"; - -export async function POST(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { repo_name } = body; - - if (!repo_name) { - return NextResponse.json({ error: "repo_name is required" }, { status: 400 }); - } - - const result = await ragIndex(repo_name); - - if (!result.success) { - return NextResponse.json({ error: result.error }, { status: 502 }); - } - - return NextResponse.json(result.data); - } catch (error) { - console.error("RAG index error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/rag/search/route.ts b/src/app/api/rag/search/route.ts deleted file mode 100644 index 142fef492056337a7fb5ad4c1a5c37eb1c125635..0000000000000000000000000000000000000000 --- a/src/app/api/rag/search/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * RAG Search Route - * - * POST /api/rag/search - * Search documents using RAG - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { ragSearch } from "@/lib/ai-client"; - -export async function POST(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const body = await request.json(); - const { query, repo_name, limit } = body; - - if (!query) { - return NextResponse.json({ error: "query is required" }, { status: 400 }); - } - - const result = await ragSearch(query, repo_name, limit || 10); - - if (!result.success) { - return NextResponse.json({ error: result.error }, { status: 502 }); - } - - return NextResponse.json(result.data); - } catch (error) { - console.error("RAG search error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/rag/suggestions/route.ts b/src/app/api/rag/suggestions/route.ts deleted file mode 100644 index cf5140a3411da9a31ee0dbe7842332e5ed8eb9ef..0000000000000000000000000000000000000000 --- a/src/app/api/rag/suggestions/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * RAG Suggestions Route - * - * GET /api/rag/suggestions - * Get suggested questions for RAG chatbot - */ - -import { NextRequest, NextResponse } from "next/server"; -import { getCurrentUser } from "@/lib/auth"; -import { callAIEngine } from "@/lib/ai-client"; - -export async function GET(request: NextRequest) { - try { - const user = await getCurrentUser(request); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const repoName = searchParams.get("repo_name"); - - // Call AI engine with empty body for suggestions - const AI_ENGINE_URL = process.env.AI_ENGINE_URL || "http://localhost:7860"; - const url = repoName - ? `${AI_ENGINE_URL}/rag/suggestions?repo_name=${encodeURIComponent(repoName)}` - : `${AI_ENGINE_URL}/rag/suggestions`; - - const response = await fetch(url); - - if (!response.ok) { - return NextResponse.json({ error: "Failed to get suggestions" }, { status: 502 }); - } - - const data = await response.json(); - return NextResponse.json(data); - } catch (error) { - console.error("RAG suggestions error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/repositories/contributor/route.ts b/src/app/api/repositories/contributor/route.ts deleted file mode 100644 index a01ff0c1d69f98212b4c6668bcfbe59e42656c8f..0000000000000000000000000000000000000000 --- a/src/app/api/repositories/contributor/route.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/db"; -import { issues, repositories } from "@/db/schema"; -import { eq, and, ne, desc, sql, count, max, inArray } from "drizzle-orm"; - -// ============================================================================= -// GET /api/repositories/contributor - Get repos where user has PRs -// ============================================================================= -// -// This is the most complex query in the original Python backend. -// It uses a MongoDB aggregation pipeline to find unique repos where -// the user has contributed PRs but is NOT the owner. -// -// Original Python aggregation pipeline (from routes/repository.py): -// -// pipeline = [ -// {"$match": {"authorName": username, "isPR": True}}, -// {"$group": { -// "_id": {"repoId": "$repoId", "repoName": "$repoName", "owner": "$owner", "repo": "$repo"}, -// "pr_count": {"$sum": 1}, -// "last_pr_at": {"$max": "$createdAt"} -// }}, -// {"$project": {...}}, -// {"$sort": {"pr_count": -1}} -// ] -// contributed_repos = await db.issues.aggregate(pipeline).to_list(1000) -// -// Drizzle SQL equivalent using GROUP BY: -// -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const username = searchParams.get("username"); - const userId = searchParams.get("userId"); - - if (!username || !userId) { - return NextResponse.json( - { error: "username and userId are required" }, - { status: 400 } - ); - } - - // Step 1: Get user's own repo IDs to exclude - const ownRepos = await db - .select({ id: repositories.id }) - .from(repositories) - .where(eq(repositories.userId, userId)); - - const ownRepoIds = ownRepos.map(r => r.id); - - // Step 2: Aggregate PRs by repo - equivalent to MongoDB $group - // SQL: SELECT repo_id, repo_name, COUNT(*) as pr_count, MAX(created_at) as last_pr_at - // FROM issues WHERE author_name = ? AND is_pr = 1 GROUP BY repo_id, repo_name, owner, repo - const contributedRepos = await db - .select({ - repoId: issues.repoId, - repoName: issues.repoName, - owner: issues.owner, - repo: issues.repo, - prCount: count().as("pr_count"), - lastPrAt: max(issues.createdAt).as("last_pr_at"), - }) - .from(issues) - .where( - and( - eq(issues.authorName, username), - eq(issues.isPR, true) - ) - ) - .groupBy(issues.repoId, issues.repoName, issues.owner, issues.repo) - .orderBy(desc(sql`pr_count`)); - - // Step 3: Filter out user's own repos - const result = contributedRepos - .filter(repo => !ownRepoIds.includes(repo.repoId)) - .map(repo => ({ - repoId: repo.repoId, - repoName: repo.repoName, - owner: repo.owner, - repo: repo.repo, - pr_count: repo.prCount, - last_pr_at: repo.lastPrAt, - role: "contributor", - name: repo.repoName || `${repo.owner}/${repo.repo}`, - })); - - return NextResponse.json( - { repos: result, count: result.length }, - { status: 200 } - ); - } catch (error) { - console.error("GET /api/repositories/contributor error:", error); - return NextResponse.json( - { error: "Failed to fetch contributor repositories" }, - { status: 500 } - ); - } -} diff --git a/src/app/api/repositories/route.ts b/src/app/api/repositories/route.ts deleted file mode 100644 index 74834c1dce6e68277a7d1145ba0838f8809b7839..0000000000000000000000000000000000000000 --- a/src/app/api/repositories/route.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/db"; -import { repositories, issues, triageData } from "@/db/schema"; -import { generateId, now } from "@/lib/utils"; -import { eq, and, desc, count, sql } from "drizzle-orm"; - -// ============================================================================= -// GET /api/repositories - Get user's repositories -// ============================================================================= -// -// Python equivalent (from routes/repository.py): -// repos = await db.repositories.find({"userId": user['id']}, {"_id": 0}).to_list(1000) -// -import { getCurrentUser } from "@/lib/auth"; - -// ... - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - let userId = searchParams.get("userId"); - - // If no userId provided, try to get the current authenticated user - if (!userId) { - const currentUser = await getCurrentUser(request); - if (currentUser) { - userId = currentUser.id; - } else { - return NextResponse.json( - { error: "userId is required or you must be logged in" }, - { status: 401 } - ); - } - } - - const repos = await db - .select() - .from(repositories) - .where(eq(repositories.userId, userId)) - .orderBy(desc(repositories.createdAt)); - - return NextResponse.json(repos, { status: 200 }); - } catch (error) { - console.error("GET /api/repositories error:", error); - return NextResponse.json( - { error: "Failed to fetch repositories" }, - { status: 500 } - ); - } -} - -// ============================================================================= -// POST /api/repositories - Add a new repository -// ============================================================================= -// -// Python equivalent (from routes/repository.py): -// repository = Repository( -// githubRepoId=repo_data['id'], -// name=request.repoFullName, -// owner=owner, -// userId=user['id'] -// ) -// repo_dict = repository.model_dump() -// repo_dict['createdAt'] = repo_dict['createdAt'].isoformat() -// await db.repositories.insert_one(repo_dict) -// -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { githubRepoId, name, owner, userId } = body; - - // Validate required fields - if (!githubRepoId || !name || !owner || !userId) { - return NextResponse.json( - { error: "Missing required fields: githubRepoId, name, owner, userId" }, - { status: 400 } - ); - } - - // Check if repository already exists for this user - const existing = await db - .select() - .from(repositories) - .where( - and( - eq(repositories.githubRepoId, githubRepoId), - eq(repositories.userId, userId) - ) - ) - .limit(1); - - if (existing.length > 0) { - return NextResponse.json( - { error: "Repository already added", repository: existing[0] }, - { status: 409 } - ); - } - - // Create new repository - const newRepo = { - id: generateId(), - githubRepoId, - name, - owner, - userId, - createdAt: now(), - }; - - await db.insert(repositories).values(newRepo); - - return NextResponse.json( - { message: "Repository added!", repository: newRepo }, - { status: 201 } - ); - } catch (error) { - console.error("POST /api/repositories error:", error); - return NextResponse.json( - { error: "Failed to add repository" }, - { status: 500 } - ); - } -} diff --git a/src/app/api/spark/badges/user/[username]/route.ts b/src/app/api/spark/badges/user/[username]/route.ts deleted file mode 100644 index fe7c07fdd30596b4c831b94ccea927ffd10a542c..0000000000000000000000000000000000000000 --- a/src/app/api/spark/badges/user/[username]/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getUserBadges } from "@/lib/db/queries/gamification"; - -export async function GET( - request: NextRequest, - context: { params: Promise<{ username: string }> } -) { - try { - const { username } = await context.params; - const badges = await getUserBadges(username); - return NextResponse.json(badges); - } catch (error) { - console.error("GET /api/spark/badges/user/:username error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/spark/gamification/calendar/[username]/route.ts b/src/app/api/spark/gamification/calendar/[username]/route.ts deleted file mode 100644 index debf87f30cf957fe0c44374fb1e405ca5c271fb1..0000000000000000000000000000000000000000 --- a/src/app/api/spark/gamification/calendar/[username]/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getUserCalendar } from "@/lib/db/queries/gamification"; - -export async function GET( - request: NextRequest, - context: { params: Promise<{ username: string }> } -) { - try { - const { username } = await context.params; - const calendar = await getUserCalendar(username); - return NextResponse.json(calendar); - } catch (error) { - console.error("GET /api/spark/gamification/calendar/:username error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/spark/gamification/streak/[username]/route.ts b/src/app/api/spark/gamification/streak/[username]/route.ts deleted file mode 100644 index df929c4ee7266e454330653099cad721ae2550e8..0000000000000000000000000000000000000000 --- a/src/app/api/spark/gamification/streak/[username]/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getUserStreak } from "@/lib/db/queries/gamification"; - -export async function GET( - request: NextRequest, - context: { params: Promise<{ username: string }> } -) { - try { - const { username } = await context.params; - const streak = await getUserStreak(username); - return NextResponse.json(streak); - } catch (error) { - console.error("GET /api/spark/gamification/streak/:username error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/src/app/api/triage/route.ts b/src/app/api/triage/route.ts deleted file mode 100644 index 28141c36f4984d88fc6fb28fcfafb19fae42be1f..0000000000000000000000000000000000000000 --- a/src/app/api/triage/route.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/db"; -import { issues, triageData } from "@/db/schema"; -import { requireAuth } from "@/lib/auth"; -import { generateId, now } from "@/lib/utils"; -import { eq } from "drizzle-orm"; - -// OpenRouter API for AI classification -const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY!; - -/** - * Classifications matching the Python backend - */ -const CLASSIFICATIONS = [ - "CRITICAL_BUG", "BUG", "FEATURE_REQUEST", "QUESTION", - "DOCS", "DUPLICATE", "NEEDS_INFO", "SPAM" -] as const; - -const SENTIMENTS = ["POSITIVE", "NEUTRAL", "NEGATIVE", "FRUSTRATED"] as const; - -/** - * POST /api/triage - * Classify an issue using AI. - * - * Request body: { issueId: string } - */ -export async function POST(request: NextRequest) { - const { user, error } = await requireAuth(request); - if (error) return error; - - try { - const body = await request.json(); - const { issueId } = body; - - if (!issueId) { - return NextResponse.json( - { error: "issueId is required" }, - { status: 400 } - ); - } - - // Fetch the issue - const issueRecords = await db - .select() - .from(issues) - .where(eq(issues.id, issueId)) - .limit(1); - - if (issueRecords.length === 0) { - return NextResponse.json( - { error: "Issue not found" }, - { status: 404 } - ); - } - - const issue = issueRecords[0]; - - // Check if already triaged - const existingTriage = await db - .select() - .from(triageData) - .where(eq(triageData.issueId, issueId)) - .limit(1); - - if (existingTriage.length > 0) { - return NextResponse.json({ - message: "Issue already triaged", - triage: existingTriage[0], - }); - } - - // Build AI prompt for classification - const prompt = `Analyze this GitHub issue and provide a classification. - -Title: ${issue.title} -Body: ${issue.body || "(empty)"} - -Respond with valid JSON only: -{ - "classification": "BUG" | "CRITICAL_BUG" | "FEATURE_REQUEST" | "QUESTION" | "DOCS" | "DUPLICATE" | "NEEDS_INFO" | "SPAM", - "sentiment": "POSITIVE" | "NEUTRAL" | "NEGATIVE" | "FRUSTRATED", - "summary": "One-line summary of the issue", - "suggestedLabel": "Suggested GitHub label (lowercase, hyphenated)" -}`; - - // Call OpenRouter API - const aiResponse = await fetch( - "https://openrouter.ai/api/v1/chat/completions", - { - method: "POST", - headers: { - Authorization: `Bearer ${OPENROUTER_API_KEY}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: "anthropic/claude-3-haiku", - messages: [{ role: "user", content: prompt }], - max_tokens: 500, - }), - } - ); - - const aiData = await aiResponse.json(); - const aiContent = aiData.choices?.[0]?.message?.content || "{}"; - - // Parse AI response - let classification = "NEEDS_INFO"; - let sentiment = "NEUTRAL"; - let summary = issue.title; - let suggestedLabel = "needs-triage"; - - try { - const parsed = JSON.parse(aiContent); - classification = CLASSIFICATIONS.includes(parsed.classification) - ? parsed.classification - : "NEEDS_INFO"; - sentiment = SENTIMENTS.includes(parsed.sentiment) - ? parsed.sentiment - : "NEUTRAL"; - summary = parsed.summary || issue.title; - suggestedLabel = parsed.suggestedLabel || "needs-triage"; - } catch { - console.error("Failed to parse AI response:", aiContent); - } - - // Save triage data - const triage = { - id: generateId(), - issueId, - classification, - summary, - suggestedLabel, - sentiment, - analyzedAt: now(), - }; - - await db.insert(triageData).values(triage); - - return NextResponse.json({ - message: "Issue triaged successfully", - triage, - }); - } catch (error) { - console.error("POST /api/triage error:", error); - return NextResponse.json( - { error: "Failed to triage issue" }, - { status: 500 } - ); - } -} - -/** - * GET /api/triage?issueId=xxx - * Get triage data for an issue. - */ -export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - const issueId = searchParams.get("issueId"); - - if (!issueId) { - return NextResponse.json( - { error: "issueId is required" }, - { status: 400 } - ); - } - - const triageRecords = await db - .select() - .from(triageData) - .where(eq(triageData.issueId, issueId)) - .limit(1); - - if (triageRecords.length === 0) { - return NextResponse.json( - { error: "Triage data not found" }, - { status: 404 } - ); - } - - return NextResponse.json(triageRecords[0]); -} diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/app/globals.css b/src/app/globals.css deleted file mode 100644 index a2dc41ecee5ec435200fe7cba2bde4107f823774..0000000000000000000000000000000000000000 --- a/src/app/globals.css +++ /dev/null @@ -1,26 +0,0 @@ -@import "tailwindcss"; - -:root { - --background: #ffffff; - --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx deleted file mode 100644 index f7fa87eb875260ed98651bc419c8139b5119e554..0000000000000000000000000000000000000000 --- a/src/app/layout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/src/app/page.tsx b/src/app/page.tsx deleted file mode 100644 index 295f8fdf14fcfe6cccaa832133037157521b1890..0000000000000000000000000000000000000000 --- a/src/app/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
- ); -} diff --git a/src/db/index.ts b/src/db/index.ts deleted file mode 100644 index cd563c0f2a70946303c48fba16da60bed6fb17c0..0000000000000000000000000000000000000000 --- a/src/db/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createClient } from "@libsql/client"; -import { drizzle } from "drizzle-orm/libsql"; -import * as schema from "./schema"; - -const client = createClient({ - url: process.env.TURSO_DATABASE_URL!, - authToken: process.env.TURSO_AUTH_TOKEN, -}); - -export const db = drizzle(client, { schema }); diff --git a/src/db/schema.ts b/src/db/schema.ts deleted file mode 100644 index 4fa2068a6a8c2bc1de35d4d0e117bdc07644fbaf..0000000000000000000000000000000000000000 --- a/src/db/schema.ts +++ /dev/null @@ -1,477 +0,0 @@ -import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core"; -import { relations } from "drizzle-orm"; - -// ============================================================================= -// ENUMS (stored as TEXT with application-level validation) -// ============================================================================= - -export const userRoleEnum = ["MAINTAINER", "CONTRIBUTOR"] as const; -export const classificationEnum = [ - "CRITICAL_BUG", "BUG", "FEATURE_REQUEST", "QUESTION", - "DOCS", "DUPLICATE", "NEEDS_INFO", "SPAM" -] as const; -export const sentimentEnum = ["POSITIVE", "NEUTRAL", "NEGATIVE", "FRUSTRATED"] as const; -export const profileVisibilityEnum = ["public", "private", "connections_only"] as const; -export const expertiseLevelEnum = ["beginner", "intermediate", "advanced", "expert"] as const; -export const sessionTypeEnum = ["one_on_one", "group", "issue_help"] as const; -export const sessionStatusEnum = ["active", "paused", "completed", "cancelled"] as const; -export const resourceTypeEnum = [ - "link", "code_snippet", "documentation", "tutorial", "tool", "example", "answer" -] as const; -export const trophyTypeEnum = [ - "first_pr", "pr_master", "pr_legend", "bug_hunter", "bug_slayer", - "first_mentor", "master_mentor", "streak_starter", "streak_warrior", "streak_legend", - "triage_helper", "triage_hero", "first_review", "review_guru", - "welcome_committee", "documentation_hero", "early_adopter", "top_contributor" -] as const; -export const trophyRarityEnum = ["common", "uncommon", "rare", "legendary"] as const; - -// ============================================================================= -// CORE TABLES -// ============================================================================= - -// ---- Users ---- -export const users = sqliteTable("users", { - id: text("id").primaryKey(), - githubId: integer("github_id").unique().notNull(), - username: text("username").notNull(), - avatarUrl: text("avatar_url").notNull(), - role: text("role"), // UserRole enum - githubAccessToken: text("github_access_token"), - createdAt: text("created_at").notNull(), - updatedAt: text("updated_at").notNull(), -}); - -// ---- Repositories ---- -export const repositories = sqliteTable("repositories", { - id: text("id").primaryKey(), - githubRepoId: integer("github_repo_id").notNull(), - name: text("name").notNull(), - owner: text("owner").notNull(), - userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), - createdAt: text("created_at").notNull(), -}); - -// ---- Issues ---- -export const issues = sqliteTable("issues", { - id: text("id").primaryKey(), - githubIssueId: integer("github_issue_id").notNull(), - number: integer("number").notNull(), - title: text("title").notNull(), - body: text("body"), - authorName: text("author_name").notNull(), - repoId: text("repo_id").notNull().references(() => repositories.id), - repoName: text("repo_name").notNull(), - owner: text("owner"), - repo: text("repo"), - htmlUrl: text("html_url"), - state: text("state").notNull().default("open"), - isPR: integer("is_pr", { mode: "boolean" }).notNull().default(false), - createdAt: text("created_at").notNull(), -}); - -// ---- Triage Data ---- -export const triageData = sqliteTable("triage_data", { - id: text("id").primaryKey(), - issueId: text("issue_id").notNull().references(() => issues.id, { onDelete: "cascade" }), - classification: text("classification").notNull(), // Classification enum - summary: text("summary").notNull(), - suggestedLabel: text("suggested_label").notNull(), - sentiment: text("sentiment").notNull(), // Sentiment enum - analyzedAt: text("analyzed_at").notNull(), -}); - -// ---- Templates ---- -export const templates = sqliteTable("templates", { - id: text("id").primaryKey(), - name: text("name").notNull(), - body: text("body").notNull(), - ownerId: text("owner_id").notNull().references(() => users.id, { onDelete: "cascade" }), - triggerClassification: text("trigger_classification"), - createdAt: text("created_at").notNull(), -}); - -// ============================================================================= -// PROFILE & MENTORSHIP TABLES -// ============================================================================= - -// ---- User Profiles ---- -export const profiles = sqliteTable("profiles", { - userId: text("user_id").primaryKey().references(() => users.id, { onDelete: "cascade" }), - username: text("username").notNull(), - avatarUrl: text("avatar_url"), - bio: text("bio"), - location: text("location"), - website: text("website"), - twitter: text("twitter"), - availableForMentoring: integer("available_for_mentoring", { mode: "boolean" }).default(false), - profileVisibility: text("profile_visibility").default("public"), - showEmail: integer("show_email", { mode: "boolean" }).default(false), - githubStats: text("github_stats"), // JSON string - statsUpdatedAt: text("stats_updated_at"), - createdAt: text("created_at").notNull(), - updatedAt: text("updated_at").notNull(), -}); - -// ---- Mentor Profiles ---- -export const mentors = sqliteTable("mentors", { - id: text("id").primaryKey(), - userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), - username: text("username").notNull(), - expertiseLevel: text("expertise_level").default("intermediate"), - availabilityHoursPerWeek: integer("availability_hours_per_week").default(5), - timezone: text("timezone"), - isActive: integer("is_active", { mode: "boolean" }).default(true), - bio: text("bio"), - avatarUrl: text("avatar_url"), - menteeCount: integer("mentee_count").default(0), - sessionsCompleted: integer("sessions_completed").default(0), - avgRating: real("avg_rating").default(0.0), - totalRatings: integer("total_ratings").default(0), - maxMentees: integer("max_mentees").default(3), - createdAt: text("created_at").notNull(), - updatedAt: text("updated_at").notNull(), -}); - -// ---- Mentor Matches ---- -export const mentorMatches = sqliteTable("mentor_matches", { - id: text("id").primaryKey(), - mentorId: text("mentor_id").notNull().references(() => mentors.id, { onDelete: "cascade" }), - mentorUsername: text("mentor_username").notNull(), - menteeId: text("mentee_id").notNull().references(() => users.id, { onDelete: "cascade" }), - menteeUsername: text("mentee_username").notNull(), - compatibilityScore: real("compatibility_score").notNull(), - matchReason: text("match_reason"), - issueId: text("issue_id"), - repoName: text("repo_name"), - status: text("status").default("suggested"), // suggested, accepted, declined, completed - createdAt: text("created_at").notNull(), -}); - -// ---- Mentorship Requests ---- -export const mentorshipRequests = sqliteTable("mentorship_requests", { - id: text("id").primaryKey(), - menteeId: text("mentee_id").notNull().references(() => users.id, { onDelete: "cascade" }), - menteeUsername: text("mentee_username"), - mentorId: text("mentor_id").notNull().references(() => mentors.id, { onDelete: "cascade" }), - mentorUsername: text("mentor_username"), - issueId: text("issue_id"), - message: text("message"), - status: text("status").default("pending"), // pending, accepted, declined - createdAt: text("created_at").notNull(), -}); - -// ---- Mentor Ratings ---- -export const mentorRatings = sqliteTable("mentor_ratings", { - id: text("id").primaryKey(), - mentorId: text("mentor_id").notNull().references(() => mentors.id, { onDelete: "cascade" }), - menteeId: text("mentee_id").notNull().references(() => users.id, { onDelete: "cascade" }), - sessionId: text("session_id"), - rating: integer("rating").notNull(), // 1-5 - feedback: text("feedback"), - createdAt: text("created_at").notNull(), -}); - -// ============================================================================= -// CHAT & MESSAGING TABLES -// ============================================================================= - -// ---- Chat Sessions ---- -export const chatSessions = sqliteTable("chat_sessions", { - id: text("id").primaryKey(), - mentorId: text("mentor_id").notNull().references(() => users.id), - mentorUsername: text("mentor_username").notNull(), - sessionType: text("session_type").default("one_on_one"), - issueId: text("issue_id"), - repoName: text("repo_name"), - topic: text("topic"), - status: text("status").default("active"), - startedAt: text("started_at").notNull(), - endedAt: text("ended_at"), - lastActivityAt: text("last_activity_at").notNull(), - summary: text("summary"), - messageCount: integer("message_count").default(0), - durationMinutes: integer("duration_minutes").default(0), -}); - -// ---- Chat Messages ---- -export const chatMessages = sqliteTable("chat_messages", { - id: text("id").primaryKey(), - sessionId: text("session_id").notNull().references(() => chatSessions.id, { onDelete: "cascade" }), - senderId: text("sender_id").notNull().references(() => users.id), - senderUsername: text("sender_username").notNull(), - isMentor: integer("is_mentor", { mode: "boolean" }).default(false), - content: text("content").notNull(), - messageType: text("message_type").default("text"), // text, code, link, file - language: text("language"), // for code blocks - isAiGenerated: integer("is_ai_generated", { mode: "boolean" }).default(false), - containsResource: integer("contains_resource", { mode: "boolean" }).default(false), - extractedResourceId: text("extracted_resource_id"), - timestamp: text("timestamp").notNull(), - editedAt: text("edited_at"), -}); - -// ---- Chat History (Legacy AI Chat) ---- -export const chatHistory = sqliteTable("chat_history", { - id: text("id").primaryKey(), - userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), - sessionId: text("session_id").notNull(), - createdAt: text("created_at").notNull(), -}); - -// ---- Issue Chats ---- -export const issueChats = sqliteTable("issue_chats", { - id: text("id").primaryKey(), - issueId: text("issue_id").notNull().references(() => issues.id, { onDelete: "cascade" }), - userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), - sessionId: text("session_id").notNull(), - createdAt: text("created_at").notNull(), - updatedAt: text("updated_at").notNull(), -}); - -// ---- Direct Messages ---- -export const messages = sqliteTable("messages", { - id: text("id").primaryKey(), - senderId: text("sender_id").notNull().references(() => users.id, { onDelete: "cascade" }), - receiverId: text("receiver_id").notNull().references(() => users.id, { onDelete: "cascade" }), - content: text("content").notNull(), - read: integer("read", { mode: "boolean" }).default(false), - timestamp: text("timestamp").notNull(), -}); - -// ============================================================================= -// GAMIFICATION TABLES -// ============================================================================= - -// ---- Trophies ---- -export const trophies = sqliteTable("trophies", { - id: text("id").primaryKey(), - userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), - username: text("username").notNull(), - trophyType: text("trophy_type").notNull(), // TrophyType enum - name: text("name").notNull(), - description: text("description").notNull(), - icon: text("icon").notNull(), - color: text("color").notNull(), - rarity: text("rarity").notNull(), // common, uncommon, rare, legendary - svgData: text("svg_data"), - isPublic: integer("is_public", { mode: "boolean" }).default(true), - shareUrl: text("share_url"), - earnedFor: text("earned_for"), // e.g., "owner/repo" - milestoneValue: integer("milestone_value"), - awardedAt: text("awarded_at").notNull(), -}); - -// ---- Resources ---- -export const resources = sqliteTable("resources", { - id: text("id").primaryKey(), - repoName: text("repo_name").notNull(), - sourceType: text("source_type").default("chat"), - sourceId: text("source_id"), - resourceType: text("resource_type").notNull(), // ResourceType enum - title: text("title").notNull(), - content: text("content").notNull(), - description: text("description"), - language: text("language"), - sharedBy: text("shared_by").notNull(), - sharedById: text("shared_by_id").notNull().references(() => users.id), - saveCount: integer("save_count").default(0), - helpfulCount: integer("helpful_count").default(0), - createdAt: text("created_at").notNull(), - updatedAt: text("updated_at").notNull(), -}); - -// ---- User Saved Resources ---- -export const userSavedResources = sqliteTable("user_saved_resources", { - id: text("id").primaryKey(), - userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), - resourceId: text("resource_id").notNull().references(() => resources.id, { onDelete: "cascade" }), - notes: text("notes"), - savedAt: text("saved_at").notNull(), -}); - -// ============================================================================= -// JUNCTION TABLES (Normalized Arrays) -// ============================================================================= - -// ---- User Repositories (User.repositories array) ---- -export const userRepositories = sqliteTable("user_repositories", { - id: text("id").primaryKey(), - userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), - repoFullName: text("repo_full_name").notNull(), - addedAt: text("added_at").notNull(), -}); - -// ---- Profile Skills ---- -export const profileSkills = sqliteTable("profile_skills", { - profileId: text("profile_id").notNull().references(() => profiles.userId, { onDelete: "cascade" }), - skill: text("skill").notNull(), -}); - -// ---- Profile Mentoring Topics ---- -export const profileMentoringTopics = sqliteTable("profile_mentoring_topics", { - profileId: text("profile_id").notNull().references(() => profiles.userId, { onDelete: "cascade" }), - topic: text("topic").notNull(), -}); - -// ---- Profile Connected Repos ---- -export const profileConnectedRepos = sqliteTable("profile_connected_repos", { - profileId: text("profile_id").notNull().references(() => profiles.userId, { onDelete: "cascade" }), - repoName: text("repo_name").notNull(), -}); - -// ---- Mentor Tech Stack ---- -export const mentorTechStack = sqliteTable("mentor_tech_stack", { - mentorId: text("mentor_id").notNull().references(() => mentors.id, { onDelete: "cascade" }), - tech: text("tech").notNull(), -}); - -// ---- Mentor Languages ---- -export const mentorLanguages = sqliteTable("mentor_languages", { - mentorId: text("mentor_id").notNull().references(() => mentors.id, { onDelete: "cascade" }), - language: text("language").notNull(), -}); - -// ---- Mentor Frameworks ---- -export const mentorFrameworks = sqliteTable("mentor_frameworks", { - mentorId: text("mentor_id").notNull().references(() => mentors.id, { onDelete: "cascade" }), - framework: text("framework").notNull(), -}); - -// ---- Mentor Preferred Topics ---- -export const mentorPreferredTopics = sqliteTable("mentor_preferred_topics", { - mentorId: text("mentor_id").notNull().references(() => mentors.id, { onDelete: "cascade" }), - topic: text("topic").notNull(), -}); - -// ---- Mentor Match Matched Skills ---- -export const mentorMatchSkills = sqliteTable("mentor_match_skills", { - matchId: text("match_id").notNull().references(() => mentorMatches.id, { onDelete: "cascade" }), - skill: text("skill").notNull(), -}); - -// ---- Chat Session Mentees ---- -export const chatSessionMentees = sqliteTable("chat_session_mentees", { - sessionId: text("session_id").notNull().references(() => chatSessions.id, { onDelete: "cascade" }), - menteeId: text("mentee_id").notNull().references(() => users.id, { onDelete: "cascade" }), - menteeUsername: text("mentee_username").notNull(), -}); - -// ---- Chat Session Key Points ---- -export const chatSessionKeyPoints = sqliteTable("chat_session_key_points", { - id: text("id").primaryKey(), - sessionId: text("session_id").notNull().references(() => chatSessions.id, { onDelete: "cascade" }), - keyPoint: text("key_point").notNull(), - sortOrder: integer("sort_order").default(0), -}); - -// ---- Chat Session Resources Shared ---- -export const chatSessionResources = sqliteTable("chat_session_resources", { - sessionId: text("session_id").notNull().references(() => chatSessions.id, { onDelete: "cascade" }), - resourceId: text("resource_id").notNull().references(() => resources.id, { onDelete: "cascade" }), -}); - -// ---- Chat History Messages ---- -export const chatHistoryMessages = sqliteTable("chat_history_messages", { - id: text("id").primaryKey(), - chatHistoryId: text("chat_history_id").notNull().references(() => chatHistory.id, { onDelete: "cascade" }), - role: text("role").notNull(), // 'user' | 'assistant' - content: text("content").notNull(), - timestamp: text("timestamp").notNull(), - githubCommentId: text("github_comment_id"), - githubCommentUrl: text("github_comment_url"), -}); - -// ---- Issue Chat Messages ---- -export const issueChatMessages = sqliteTable("issue_chat_messages", { - id: text("id").primaryKey(), - issueChatId: text("issue_chat_id").notNull().references(() => issueChats.id, { onDelete: "cascade" }), - role: text("role").notNull(), - content: text("content").notNull(), - timestamp: text("timestamp").notNull(), - githubCommentId: text("github_comment_id"), - githubCommentUrl: text("github_comment_url"), -}); - -// ---- Chat Message Attachments ---- -export const chatMessageAttachments = sqliteTable("chat_message_attachments", { - messageId: text("message_id").notNull().references(() => chatMessages.id, { onDelete: "cascade" }), - attachment: text("attachment").notNull(), -}); - -// ---- Chat Message Reactions ---- -export const chatMessageReactions = sqliteTable("chat_message_reactions", { - messageId: text("message_id").notNull().references(() => chatMessages.id, { onDelete: "cascade" }), - emoji: text("emoji").notNull(), - userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), -}); - -// ---- Resource Tags ---- -export const resourceTags = sqliteTable("resource_tags", { - resourceId: text("resource_id").notNull().references(() => resources.id, { onDelete: "cascade" }), - tag: text("tag").notNull(), -}); - -// ============================================================================= -// RELATIONS (for Drizzle Query API) -// ============================================================================= - -export const usersRelations = relations(users, ({ many, one }) => ({ - repositories: many(repositories), - issues: many(issues), - profile: one(profiles), - trophies: many(trophies), - sentMessages: many(messages, { relationName: "sender" }), - receivedMessages: many(messages, { relationName: "receiver" }), -})); - -export const repositoriesRelations = relations(repositories, ({ one, many }) => ({ - user: one(users, { fields: [repositories.userId], references: [users.id] }), - issues: many(issues), -})); - -export const issuesRelations = relations(issues, ({ one, many }) => ({ - repository: one(repositories, { fields: [issues.repoId], references: [repositories.id] }), - triageData: one(triageData), - issueChats: many(issueChats), -})); - -export const triageDataRelations = relations(triageData, ({ one }) => ({ - issue: one(issues, { fields: [triageData.issueId], references: [issues.id] }), -})); - -export const profilesRelations = relations(profiles, ({ one, many }) => ({ - user: one(users, { fields: [profiles.userId], references: [users.id] }), - skills: many(profileSkills), - mentoringTopics: many(profileMentoringTopics), - connectedRepos: many(profileConnectedRepos), -})); - -export const mentorsRelations = relations(mentors, ({ one, many }) => ({ - user: one(users, { fields: [mentors.userId], references: [users.id] }), - matches: many(mentorMatches), - requests: many(mentorshipRequests), - ratings: many(mentorRatings), - techStack: many(mentorTechStack), - languages: many(mentorLanguages), - frameworks: many(mentorFrameworks), - preferredTopics: many(mentorPreferredTopics), -})); - -export const chatSessionsRelations = relations(chatSessions, ({ one, many }) => ({ - mentor: one(users, { fields: [chatSessions.mentorId], references: [users.id] }), - messages: many(chatMessages), - mentees: many(chatSessionMentees), - keyPoints: many(chatSessionKeyPoints), - sharedResources: many(chatSessionResources), -})); - -export const trophiesRelations = relations(trophies, ({ one }) => ({ - user: one(users, { fields: [trophies.userId], references: [users.id] }), -})); - -export const resourcesRelations = relations(resources, ({ one, many }) => ({ - sharer: one(users, { fields: [resources.sharedById], references: [users.id] }), - tags: many(resourceTags), - savedBy: many(userSavedResources), -})); diff --git a/src/lib/ai-client.ts b/src/lib/ai-client.ts deleted file mode 100644 index 3e9beeb2754aad74b95e48978d2c51d6b67198c5..0000000000000000000000000000000000000000 --- a/src/lib/ai-client.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * AI Engine Client - * - * Helper to proxy requests to the external Python AI Engine. - * Configure AI_ENGINE_URL in .env.local - */ - -const AI_ENGINE_URL = process.env.AI_ENGINE_URL || "http://localhost:7860"; - -interface AIResponse { - success: boolean; - data?: T; - error?: string; -} - -/** - * Call the AI Engine with type safety - */ -export async function callAIEngine( - endpoint: string, - body: object, - options?: { timeout?: number } -): Promise> { - const url = `${AI_ENGINE_URL}${endpoint}`; - const timeout = options?.timeout || 30000; - - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - const error = await response.text(); - return { success: false, error: `AI Engine error: ${response.status} - ${error}` }; - } - - const data = await response.json(); - return { success: true, data: data as T }; - } catch (error) { - if (error instanceof Error && error.name === "AbortError") { - return { success: false, error: "AI Engine request timed out" }; - } - return { success: false, error: `Failed to reach AI Engine: ${error}` }; - } -} - -// Pre-configured helpers for each service - -export async function triageIssue(issue: { - title: string; - body?: string; - authorName?: string; - isPR?: boolean; -}) { - return callAIEngine("/triage", { - title: issue.title, - body: issue.body || "", - authorName: issue.authorName || "unknown", - isPR: issue.isPR || false, - }); -} - -export async function chat(message: string, history?: { role: string; content: string }[], context?: object) { - return callAIEngine("/chat", { message, history, context }); -} - -export async function ragQuery(question: string, repoName?: string) { - return callAIEngine("/rag/chat", { question, repo_name: repoName }); -} - -export async function findMentorMatches(userId: string, username: string, limit = 5) { - return callAIEngine("/mentor-match", { user_id: userId, username, limit }); -} - -export async function generateHype(pr: { - title: string; - body?: string; - filesChanged?: string[]; - additions?: number; - deletions?: number; -}) { - return callAIEngine("/hype", { - pr_title: pr.title, - pr_body: pr.body || "", - files_changed: pr.filesChanged || [], - additions: pr.additions || 0, - deletions: pr.deletions || 0, - }); -} - -export async function ragChat(question: string, repoName?: string, topK = 5) { - return callAIEngine("/rag/chat", { question, repo_name: repoName, top_k: topK }); -} - -export async function ragIndex(repoName: string, githubAccessToken?: string) { - return callAIEngine("/rag/index", { repo_name: repoName, github_access_token: githubAccessToken }); -} - -export async function ragSearch(query: string, repoName?: string, limit = 10) { - return callAIEngine("/rag/search", { query, repo_name: repoName, limit }); -} - diff --git a/src/lib/auth.ts b/src/lib/auth.ts deleted file mode 100644 index 15f140b3e3543688c730fd05357f5483ac079aa5..0000000000000000000000000000000000000000 --- a/src/lib/auth.ts +++ /dev/null @@ -1,85 +0,0 @@ -import jwt from "jsonwebtoken"; -import { NextRequest, NextResponse } from "next/server"; -import { db } from "@/db"; -import { users } from "@/db/schema"; -import { eq } from "drizzle-orm"; - -const JWT_SECRET = process.env.JWT_SECRET!; - -/** - * Create a JWT token for a user. - */ -export function createJwtToken(userId: string, role: string | null): string { - const payload = { - user_id: userId, - role: role, - exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, // 30 days - }; - return jwt.sign(payload, JWT_SECRET, { algorithm: "HS256" }); -} - -/** - * Verify and decode a JWT token. - */ -export function verifyJwtToken(token: string): { user_id: string; role: string | null } { - try { - const payload = jwt.verify(token, JWT_SECRET, { algorithms: ["HS256"] }) as { - user_id: string; - role: string | null; - }; - return payload; - } catch (error) { - throw new Error("Invalid or expired token"); - } -} - -/** - * Extract user from Authorization header. - */ -export async function getCurrentUser(request: NextRequest) { - const authHeader = request.headers.get("Authorization"); - - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return null; - } - - const token = authHeader.substring(7); - - try { - const payload = verifyJwtToken(token); - - // Fetch full user from database - const userRecords = await db - .select() - .from(users) - .where(eq(users.id, payload.user_id)) - .limit(1); - - if (userRecords.length === 0) { - return null; - } - - return userRecords[0]; - } catch { - return null; - } -} - -/** - * Helper to require authentication on a route. - */ -export async function requireAuth(request: NextRequest) { - const user = await getCurrentUser(request); - - if (!user) { - return { - user: null, - error: NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ), - }; - } - - return { user, error: null }; -} diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts deleted file mode 100644 index d3cb5028d28091e4bf7eee62588bf644939cecfd..0000000000000000000000000000000000000000 --- a/src/lib/db/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Database Queries Index - * - * Re-exports all query functions from the queries folder. - */ - -export * from "./queries/users"; -export * from "./queries/repositories"; -export * from "./queries/issues"; -export * from "./queries/messages"; -export * from "./queries/templates"; diff --git a/src/lib/db/queries/gamification.ts b/src/lib/db/queries/gamification.ts deleted file mode 100644 index 8421ba0402595f6f0e0c403734cf00752544f22d..0000000000000000000000000000000000000000 --- a/src/lib/db/queries/gamification.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Gamification Queries - Drizzle ORM - */ - -import { db } from "@/db"; -import { trophies, users, issues } from "@/db/schema"; -import { eq, desc, and, count, not, sql } from "drizzle-orm"; - -// ============================================================================= -// Badges / Trophies -// ============================================================================= - -export async function getUserBadges(username: string) { - const user = await db.select().from(users).where(eq(users.username, username)).limit(1); - if (!user[0]) return []; - - return db.select() - .from(trophies) - .where(eq(trophies.userId, user[0].id)) - .orderBy(desc(trophies.awardedAt)); -} - -// ============================================================================= -// Streak -// ============================================================================= - -export async function getUserStreak(username: string) { - // This is a simplified streak calculation based on issue/PR creation dates - // In a real app, you might want to track daily activity explicitly - - const user = await db.select().from(users).where(eq(users.username, username)).limit(1); - if (!user[0]) return { currentStreak: 0, longestStreak: 0 }; - - const activity = await db.select({ - createdAt: issues.createdAt - }) - .from(issues) - .leftJoin(users, eq(issues.authorName, users.username)) // Assuming authorName matches username - .where(eq(users.username, username)) - .orderBy(desc(issues.createdAt)); - - if (activity.length === 0) return { currentStreak: 0, longestStreak: 0 }; - - // Calculate streak logic here (simplified for now) - // For now returning mock data or simple calculation as true streak logic can be complex - // pending actual "daily activity" table - - return { - currentStreak: 0, - longestStreak: 0 - }; -} - -// ============================================================================= -// Clean Calendar Data -// ============================================================================= - -export async function getUserCalendar(username: string) { - const user = await db.select().from(users).where(eq(users.username, username)).limit(1); - if (!user[0]) return []; - - // Aggregate issues created by day - const activity = await db.select({ - day: sql`substr(${issues.createdAt}, 1, 10)`, - value: count() - }) - .from(issues) - .leftJoin(users, eq(issues.authorName, users.username)) - .where(eq(users.username, username)) - .groupBy(sql`substr(${issues.createdAt}, 1, 10)`) - .orderBy(sql`day`); - - return activity; -} diff --git a/src/lib/db/queries/issues.ts b/src/lib/db/queries/issues.ts deleted file mode 100644 index 07586b19c81343966f4c18421174ffc8ac8205fb..0000000000000000000000000000000000000000 --- a/src/lib/db/queries/issues.ts +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Issue Queries - Drizzle ORM - * - * All issue and triage-related database operations. - */ - -import { db } from "@/db"; -import { issues, triageData, repositories } from "@/db/schema"; -import { eq, and, desc, asc, count, or, like, sql } from "drizzle-orm"; -import { v4 as uuidv4 } from "uuid"; - -// ============================================================================= -// Issue CRUD -// ============================================================================= - -export async function getIssueById(id: string) { - const result = await db.select().from(issues).where(eq(issues.id, id)).limit(1); - return result[0] || null; -} - -export async function getIssueByGithubId(githubIssueId: number) { - const result = await db.select().from(issues).where(eq(issues.githubIssueId, githubIssueId)).limit(1); - return result[0] || null; -} - -export async function createIssue(data: { - githubIssueId: number; - number: number; - title: string; - body?: string; - authorName: string; - repoId: string; - repoName: string; - owner?: string; - repo?: string; - htmlUrl?: string; - state?: string; - isPR?: boolean; -}) { - const id = uuidv4(); - const now = new Date().toISOString(); - - await db.insert(issues).values({ - id, - githubIssueId: data.githubIssueId, - number: data.number, - title: data.title, - body: data.body || null, - authorName: data.authorName, - repoId: data.repoId, - repoName: data.repoName, - owner: data.owner || null, - repo: data.repo || null, - htmlUrl: data.htmlUrl || null, - state: data.state || "open", - isPR: data.isPR || false, - createdAt: now, - }).onConflictDoNothing(); - - return { id, ...data, createdAt: now }; -} - -export async function updateIssueState(id: string, state: string) { - await db.update(issues).set({ state }).where(eq(issues.id, id)); -} - -// ============================================================================= -// Issue Listing with Pagination -// ============================================================================= - -export interface IssueFilters { - repoId?: string; - userId?: string; - authorName?: string; - state?: string; - isPR?: boolean; - search?: string; -} - -export async function getIssues(filters: IssueFilters, page = 1, limit = 10) { - const offset = (page - 1) * limit; - - // Build conditions - const conditions = []; - if (filters.repoId) conditions.push(eq(issues.repoId, filters.repoId)); - if (filters.authorName) conditions.push(eq(issues.authorName, filters.authorName)); - if (filters.state) conditions.push(eq(issues.state, filters.state)); - if (filters.isPR !== undefined) conditions.push(eq(issues.isPR, filters.isPR)); - if (filters.search) { - conditions.push(or( - like(issues.title, `%${filters.search}%`), - like(issues.body, `%${filters.search}%`) - )); - } - - // If userId provided, filter by user's repos - if (filters.userId) { - const userRepos = await db.select({ id: repositories.id }) - .from(repositories) - .where(eq(repositories.userId, filters.userId)); - const repoIds = userRepos.map(r => r.id); - - if (repoIds.length > 0) { - conditions.push(sql`${issues.repoId} IN (${sql.join(repoIds.map(id => sql`${id}`), sql`, `)})`); - } else { - return { issues: [], total: 0, page, limit, totalPages: 0 }; - } - } - - const whereClause = conditions.length > 0 ? and(...conditions) : undefined; - - const results = await db.select() - .from(issues) - .where(whereClause) - .orderBy(desc(issues.createdAt)) - .limit(limit) - .offset(offset); - - const totalResult = await db.select({ count: count() }) - .from(issues) - .where(whereClause); - - const total = totalResult[0]?.count || 0; - - return { - issues: results, - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }; -} - -// ============================================================================= -// Issues with Triage Data -// ============================================================================= - -export async function getIssuesWithTriage(filters: IssueFilters, page = 1, limit = 10) { - const { issues: issueList, total, totalPages } = await getIssues(filters, page, limit); - - // Fetch triage data for each issue - const issuesWithTriage = await Promise.all(issueList.map(async (issue) => { - const triage = await db.select() - .from(triageData) - .where(eq(triageData.issueId, issue.id)) - .limit(1); - - return { - ...issue, - triage: triage[0] || null, - }; - })); - - return { issues: issuesWithTriage, total, page, limit, totalPages }; -} - -// ============================================================================= -// Triage Operations -// ============================================================================= - -export async function getTriageData(issueId: string) { - const result = await db.select().from(triageData).where(eq(triageData.issueId, issueId)).limit(1); - return result[0] || null; -} - -export async function createOrUpdateTriageData(data: { - issueId: string; - classification: string; - summary: string; - suggestedLabel: string; - sentiment: string; -}) { - const id = uuidv4(); - const now = new Date().toISOString(); - - await db.insert(triageData).values({ - id, - issueId: data.issueId, - classification: data.classification, - summary: data.summary, - suggestedLabel: data.suggestedLabel, - sentiment: data.sentiment, - analyzedAt: now, - }).onConflictDoUpdate({ - target: triageData.issueId, - set: { - classification: data.classification, - summary: data.summary, - suggestedLabel: data.suggestedLabel, - sentiment: data.sentiment, - analyzedAt: now, - } - }); - - return { id, ...data, analyzedAt: now }; -} - -// ============================================================================= -// Dashboard Stats -// ============================================================================= - -export async function getDashboardStats(userId: string) { - const userRepos = await db.select({ id: repositories.id }) - .from(repositories) - .where(eq(repositories.userId, userId)); - - if (userRepos.length === 0) { - return { openIssues: 0, openPRs: 0, triaged: 0, untriaged: 0 }; - } - - const repoIds = userRepos.map(r => r.id); - const repoCondition = sql`${issues.repoId} IN (${sql.join(repoIds.map(id => sql`${id}`), sql`, `)})`; - - const openIssues = await db.select({ count: count() }) - .from(issues) - .where(and(repoCondition, eq(issues.state, "open"), eq(issues.isPR, false))); - - const openPRs = await db.select({ count: count() }) - .from(issues) - .where(and(repoCondition, eq(issues.state, "open"), eq(issues.isPR, true))); - - // Count triaged issues - const allOpenIssueIds = await db.select({ id: issues.id }) - .from(issues) - .where(and(repoCondition, eq(issues.state, "open"))); - - let triaged = 0; - for (const issue of allOpenIssueIds) { - const hasTriagedata = await db.select({ id: triageData.id }) - .from(triageData) - .where(eq(triageData.issueId, issue.id)) - .limit(1); - if (hasTriagedata.length > 0) triaged++; - } - - return { - openIssues: openIssues[0]?.count || 0, - openPRs: openPRs[0]?.count || 0, - triaged, - untriaged: (openIssues[0]?.count || 0) - triaged, - }; -} diff --git a/src/lib/db/queries/messages.ts b/src/lib/db/queries/messages.ts deleted file mode 100644 index 867844394b7ab0e60777b1741348af6c5e0ae317..0000000000000000000000000000000000000000 --- a/src/lib/db/queries/messages.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Message Queries - Drizzle ORM - * - * All messaging-related database operations. - */ - -import { db } from "@/db"; -import { messages, users } from "@/db/schema"; -import { eq, and, or, desc, count, gt, asc } from "drizzle-orm"; -import { v4 as uuidv4 } from "uuid"; - -// ============================================================================= -// Message CRUD -// ============================================================================= - -export async function sendMessage(data: { - senderId: string; - receiverId: string; - content: string; -}) { - const id = uuidv4(); - const now = new Date().toISOString(); - - await db.insert(messages).values({ - id, - senderId: data.senderId, - receiverId: data.receiverId, - content: data.content, - read: false, - timestamp: now, - }); - - return { id, ...data, read: false, timestamp: now }; -} - -export async function markMessagesAsRead(currentUserId: string, otherUserId: string) { - await db.update(messages) - .set({ read: true }) - .where( - and( - eq(messages.senderId, otherUserId), - eq(messages.receiverId, currentUserId), - eq(messages.read, false) - ) - ); -} - -// ============================================================================= -// Chat History -// ============================================================================= - -export async function getChatHistory(currentUserId: string, otherUserId: string, limit = 50) { - const history = await db.select() - .from(messages) - .where( - or( - and(eq(messages.senderId, currentUserId), eq(messages.receiverId, otherUserId)), - and(eq(messages.senderId, otherUserId), eq(messages.receiverId, currentUserId)) - ) - ) - .orderBy(asc(messages.timestamp)) - .limit(limit); - - return history; -} - -export async function pollNewMessages(currentUserId: string, otherUserId: string, lastMessageId?: string) { - let query = db.select() - .from(messages) - .where( - and( - eq(messages.senderId, otherUserId), - eq(messages.receiverId, currentUserId) - ) - ) - .orderBy(asc(messages.timestamp)); - - // If we have a last message ID, only get newer messages - if (lastMessageId) { - const lastMessage = await db.select({ timestamp: messages.timestamp }) - .from(messages) - .where(eq(messages.id, lastMessageId)) - .limit(1); - - if (lastMessage[0]) { - query = db.select() - .from(messages) - .where( - and( - eq(messages.senderId, otherUserId), - eq(messages.receiverId, currentUserId), - gt(messages.timestamp, lastMessage[0].timestamp) - ) - ) - .orderBy(asc(messages.timestamp)); - } - } - - return query; -} - -// ============================================================================= -// Conversations -// ============================================================================= - -export async function getConversations(currentUserId: string) { - // Get all unique conversation partners - const sentTo = await db.selectDistinct({ partnerId: messages.receiverId }) - .from(messages) - .where(eq(messages.senderId, currentUserId)); - - const receivedFrom = await db.selectDistinct({ partnerId: messages.senderId }) - .from(messages) - .where(eq(messages.receiverId, currentUserId)); - - const partnerIds = new Set([ - ...sentTo.map(m => m.partnerId), - ...receivedFrom.map(m => m.partnerId) - ]); - - const conversations = await Promise.all([...partnerIds].map(async (partnerId) => { - // Get partner info - const partner = await db.select() - .from(users) - .where(eq(users.id, partnerId)) - .limit(1); - - // Get last message - const lastMessage = await db.select() - .from(messages) - .where( - or( - and(eq(messages.senderId, currentUserId), eq(messages.receiverId, partnerId)), - and(eq(messages.senderId, partnerId), eq(messages.receiverId, currentUserId)) - ) - ) - .orderBy(desc(messages.timestamp)) - .limit(1); - - // Count unread - const unreadCount = await db.select({ count: count() }) - .from(messages) - .where( - and( - eq(messages.senderId, partnerId), - eq(messages.receiverId, currentUserId), - eq(messages.read, false) - ) - ); - - return { - partnerId, - partnerUsername: partner[0]?.username || "Unknown", - partnerAvatar: partner[0]?.avatarUrl || null, - lastMessage: lastMessage[0] || null, - unreadCount: unreadCount[0]?.count || 0, - }; - })); - - // Sort by last message timestamp - return conversations.sort((a, b) => { - const aTime = a.lastMessage?.timestamp || ""; - const bTime = b.lastMessage?.timestamp || ""; - return bTime.localeCompare(aTime); - }); -} - -// ============================================================================= -// Unread Count -// ============================================================================= - -export async function getUnreadCount(userId: string) { - const result = await db.select({ count: count() }) - .from(messages) - .where(and(eq(messages.receiverId, userId), eq(messages.read, false))); - - return result[0]?.count || 0; -} diff --git a/src/lib/db/queries/repositories.ts b/src/lib/db/queries/repositories.ts deleted file mode 100644 index e58d1184e2e3c17318b9edf392f5ded22a54ffd4..0000000000000000000000000000000000000000 --- a/src/lib/db/queries/repositories.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Repository Queries - Drizzle ORM - * - * All repository-related database operations. - */ - -import { db } from "@/db"; -import { repositories, issues, users } from "@/db/schema"; -import { eq, and, desc, count, sql } from "drizzle-orm"; -import { v4 as uuidv4 } from "uuid"; - -// ============================================================================= -// Repository CRUD -// ============================================================================= - -export async function getRepositoryById(id: string) { - const result = await db.select().from(repositories).where(eq(repositories.id, id)).limit(1); - return result[0] || null; -} - -export async function getRepositoryByGithubId(githubRepoId: number) { - const result = await db.select().from(repositories).where(eq(repositories.githubRepoId, githubRepoId)).limit(1); - return result[0] || null; -} - -export async function getRepositoriesByUserId(userId: string) { - return db.select().from(repositories).where(eq(repositories.userId, userId)).orderBy(desc(repositories.createdAt)); -} - -export async function createRepository(data: { - githubRepoId: number; - name: string; - owner: string; - userId: string; -}) { - const id = uuidv4(); - const now = new Date().toISOString(); - - await db.insert(repositories).values({ - id, - githubRepoId: data.githubRepoId, - name: data.name, - owner: data.owner, - userId: data.userId, - createdAt: now, - }).onConflictDoNothing(); - - return { id, ...data, createdAt: now }; -} - -export async function deleteRepository(id: string) { - await db.delete(repositories).where(eq(repositories.id, id)); -} - -// ============================================================================= -// Maintainer Repositories -// ============================================================================= - -export async function getMaintainerRepositories(userId: string) { - const repos = await db.select().from(repositories).where(eq(repositories.userId, userId)); - - // Add issue counts - const reposWithCounts = await Promise.all(repos.map(async (repo) => { - const issueCount = await db.select({ count: count() }) - .from(issues) - .where(and(eq(issues.repoId, repo.id), eq(issues.state, "open"))); - - const prCount = await db.select({ count: count() }) - .from(issues) - .where(and(eq(issues.repoId, repo.id), eq(issues.isPR, true), eq(issues.state, "open"))); - - return { - ...repo, - fullName: `${repo.owner}/${repo.name}`, - openIssues: issueCount[0]?.count || 0, - openPRs: prCount[0]?.count || 0, - }; - })); - - return reposWithCounts; -} - -// ============================================================================= -// Contributor Repositories -// ============================================================================= - -export async function getContributorRepositories(userId: string, username: string) { - // Get repos where user has contributed (authored issues/PRs) but doesn't own - const userRepos = await db.select({ id: repositories.id }).from(repositories).where(eq(repositories.userId, userId)); - const userRepoIds = new Set(userRepos.map(r => r.id)); - - const contributedIssues = await db.select({ - repoId: issues.repoId, - repoName: issues.repoName, - owner: issues.owner, - repo: issues.repo, - }) - .from(issues) - .where(eq(issues.authorName, username)) - .groupBy(issues.repoId, issues.repoName, issues.owner, issues.repo); - - // Filter out owned repos - const contributedRepos = contributedIssues.filter(issue => !userRepoIds.has(issue.repoId)); - - // Get counts for each repo - const reposWithCounts = await Promise.all(contributedRepos.map(async (repo) => { - const myIssues = await db.select({ count: count() }) - .from(issues) - .where(and(eq(issues.repoId, repo.repoId), eq(issues.authorName, username))); - - return { - id: repo.repoId, - name: repo.repo || repo.repoName.split("/")[1], - owner: repo.owner || repo.repoName.split("/")[0], - fullName: repo.repoName, - myContributions: myIssues[0]?.count || 0, - }; - })); - - return reposWithCounts; -} - -// ============================================================================= -// Dashboard Stats -// ============================================================================= - -export async function getRepositoryStats(userId: string) { - const repos = await getRepositoriesByUserId(userId); - const repoIds = repos.map(r => r.id); - - if (repoIds.length === 0) { - return { totalRepos: 0, totalIssues: 0, totalPRs: 0, openIssues: 0, openPRs: 0 }; - } - - const allIssues = await db.select({ - id: issues.id, - isPR: issues.isPR, - state: issues.state - }).from(issues).where(sql`${issues.repoId} IN (${sql.join(repoIds.map(id => sql`${id}`), sql`, `)})`); - - const totalIssues = allIssues.filter(i => !i.isPR).length; - const totalPRs = allIssues.filter(i => i.isPR).length; - const openIssues = allIssues.filter(i => !i.isPR && i.state === "open").length; - const openPRs = allIssues.filter(i => i.isPR && i.state === "open").length; - - return { - totalRepos: repos.length, - totalIssues, - totalPRs, - openIssues, - openPRs, - }; -} diff --git a/src/lib/db/queries/templates.ts b/src/lib/db/queries/templates.ts deleted file mode 100644 index 0d6add925224df8760618ce944f5bb7f3f859329..0000000000000000000000000000000000000000 --- a/src/lib/db/queries/templates.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Template Queries - Drizzle ORM - * - * All template-related database operations. - */ - -import { db } from "@/db"; -import { templates } from "@/db/schema"; -import { eq, desc, and } from "drizzle-orm"; -import { v4 as uuidv4 } from "uuid"; - -// ============================================================================= -// Template CRUD -// ============================================================================= - -export async function getTemplatesByOwnerId(ownerId: string) { - return db.select() - .from(templates) - .where(eq(templates.ownerId, ownerId)) - .orderBy(desc(templates.createdAt)); -} - -export async function getTemplateById(id: string) { - const result = await db.select() - .from(templates) - .where(eq(templates.id, id)) - .limit(1); - return result[0] || null; -} - -export async function createTemplate(data: { - name: string; - body: string; - ownerId: string; - triggerClassification?: string; -}) { - const id = uuidv4(); - const now = new Date().toISOString(); - - await db.insert(templates).values({ - id, - name: data.name, - body: data.body, - ownerId: data.ownerId, - triggerClassification: data.triggerClassification || null, - createdAt: now, - }); - - return { id, ...data, createdAt: now }; -} - -export async function updateTemplate(id: string, data: Partial<{ - name: string; - body: string; - triggerClassification: string; -}>) { - await db.update(templates).set(data).where(eq(templates.id, id)); -} - -export async function deleteTemplate(id: string) { - await db.delete(templates).where(eq(templates.id, id)); -} - -export async function getTemplateByClassification(ownerId: string, classification: string) { - const result = await db.select() - .from(templates) - .where(and( - eq(templates.ownerId, ownerId), - eq(templates.triggerClassification, classification) - )) - .limit(1); - return result[0] || null; -} diff --git a/src/lib/db/queries/users.ts b/src/lib/db/queries/users.ts deleted file mode 100644 index c68f759be03a9c4689f36a762cdce5546e434e35..0000000000000000000000000000000000000000 --- a/src/lib/db/queries/users.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * User Queries - Drizzle ORM - * - * All user-related database operations. - */ - -import { db } from "@/db"; -import { users, profiles, profileSkills, profileMentoringTopics, profileConnectedRepos, userRepositories } from "@/db/schema"; -import { eq, and, like, desc } from "drizzle-orm"; -import { v4 as uuidv4 } from "uuid"; - -// ============================================================================= -// User CRUD -// ============================================================================= - -export async function getUserById(id: string) { - const result = await db.select().from(users).where(eq(users.id, id)).limit(1); - return result[0] || null; -} - -export async function getUserByGithubId(githubId: number) { - const result = await db.select().from(users).where(eq(users.githubId, githubId)).limit(1); - return result[0] || null; -} - -export async function getUserByUsername(username: string) { - const result = await db.select().from(users).where(eq(users.username, username)).limit(1); - return result[0] || null; -} - -export async function createUser(data: { - githubId: number; - username: string; - avatarUrl: string; - role?: string; - githubAccessToken?: string; -}) { - const now = new Date().toISOString(); - const id = uuidv4(); - - await db.insert(users).values({ - id, - githubId: data.githubId, - username: data.username, - avatarUrl: data.avatarUrl, - role: data.role || null, - githubAccessToken: data.githubAccessToken || null, - createdAt: now, - updatedAt: now, - }); - - return { id, ...data, createdAt: now, updatedAt: now }; -} - -export async function updateUser(id: string, data: Partial<{ - username: string; - avatarUrl: string; - role: string; - githubAccessToken: string; -}>) { - await db.update(users) - .set({ ...data, updatedAt: new Date().toISOString() }) - .where(eq(users.id, id)); -} - -export async function updateUserRole(id: string, role: string) { - await db.update(users) - .set({ role, updatedAt: new Date().toISOString() }) - .where(eq(users.id, id)); -} - -// ============================================================================= -// Profile Operations -// ============================================================================= - -export async function getProfile(userId: string) { - const profile = await db.select().from(profiles).where(eq(profiles.userId, userId)).limit(1); - if (!profile[0]) return null; - - const skills = await db.select().from(profileSkills).where(eq(profileSkills.profileId, userId)); - const topics = await db.select().from(profileMentoringTopics).where(eq(profileMentoringTopics.profileId, userId)); - const repos = await db.select().from(profileConnectedRepos).where(eq(profileConnectedRepos.profileId, userId)); - - return { - ...profile[0], - skills: skills.map(s => s.skill), - mentoringTopics: topics.map(t => t.topic), - connectedRepos: repos.map(r => r.repoName), - }; -} - -export async function getProfileByUsername(username: string) { - const profile = await db.select().from(profiles).where(eq(profiles.username, username)).limit(1); - if (!profile[0]) return null; - - const userId = profile[0].userId; - const skills = await db.select().from(profileSkills).where(eq(profileSkills.profileId, userId)); - const topics = await db.select().from(profileMentoringTopics).where(eq(profileMentoringTopics.profileId, userId)); - - return { - ...profile[0], - skills: skills.map(s => s.skill), - mentoringTopics: topics.map(t => t.topic), - }; -} - -export async function createOrUpdateProfile(userId: string, data: { - username: string; - avatarUrl?: string; - bio?: string; - location?: string; - website?: string; - twitter?: string; - availableForMentoring?: boolean; - profileVisibility?: string; - showEmail?: boolean; - skills?: string[]; - mentoringTopics?: string[]; -}) { - const now = new Date().toISOString(); - - // Upsert profile - await db.insert(profiles).values({ - userId, - username: data.username, - avatarUrl: data.avatarUrl || null, - bio: data.bio || null, - location: data.location || null, - website: data.website || null, - twitter: data.twitter || null, - availableForMentoring: data.availableForMentoring || false, - profileVisibility: data.profileVisibility || "public", - showEmail: data.showEmail || false, - createdAt: now, - updatedAt: now, - }).onConflictDoUpdate({ - target: profiles.userId, - set: { - bio: data.bio, - location: data.location, - website: data.website, - twitter: data.twitter, - availableForMentoring: data.availableForMentoring, - profileVisibility: data.profileVisibility, - showEmail: data.showEmail, - updatedAt: now, - } - }); - - // Update skills - if (data.skills) { - await db.delete(profileSkills).where(eq(profileSkills.profileId, userId)); - for (const skill of data.skills) { - await db.insert(profileSkills).values({ profileId: userId, skill }); - } - } - - // Update topics - if (data.mentoringTopics) { - await db.delete(profileMentoringTopics).where(eq(profileMentoringTopics.profileId, userId)); - for (const topic of data.mentoringTopics) { - await db.insert(profileMentoringTopics).values({ profileId: userId, topic }); - } - } - - return getProfile(userId); -} - -// ============================================================================= -// User Stats -// ============================================================================= - -export async function getUserRepositories(userId: string) { - return db.select().from(userRepositories).where(eq(userRepositories.userId, userId)); -} - -export async function addUserRepository(userId: string, repoFullName: string) { - await db.insert(userRepositories).values({ - id: uuidv4(), - userId, - repoFullName, - addedAt: new Date().toISOString(), - }).onConflictDoNothing(); -} - -export async function removeUserRepository(userId: string, repoFullName: string) { - await db.delete(userRepositories).where( - and( - eq(userRepositories.userId, userId), - eq(userRepositories.repoFullName, repoFullName) - ) - ); -} diff --git a/src/lib/utils.ts b/src/lib/utils.ts deleted file mode 100644 index f9816f4f2b2f2ee969c0e9d642647d78afeba7db..0000000000000000000000000000000000000000 --- a/src/lib/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { v4 as uuidv4 } from "uuid"; - -/** - * Generates a UUID string for use as primary keys. - * Preserves the same format as the Python backend. - */ -export function generateId(): string { - return uuidv4(); -} - -/** - * Returns the current ISO 8601 timestamp. - */ -export function now(): string { - return new Date().toISOString(); -} diff --git a/src/middleware.ts b/src/middleware.ts deleted file mode 100644 index d0546e805802b9e036cd707f6ac86ae5c44221a9..0000000000000000000000000000000000000000 --- a/src/middleware.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; - -// CORS configuration -const allowedOrigins = [ - "http://localhost:5173", // Vite dev - "http://localhost:3000", // Next.js dev - "https://open-triage.vercel.app", - "https://opentriage.onrender.com", -]; - -export function middleware(request: NextRequest) { - const origin = request.headers.get("origin") || ""; - const isAllowedOrigin = allowedOrigins.includes(origin); - - // Handle preflight requests - if (request.method === "OPTIONS") { - return new NextResponse(null, { - status: 204, - headers: { - "Access-Control-Allow-Origin": isAllowedOrigin ? origin : "", - "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization", - "Access-Control-Max-Age": "86400", - }, - }); - } - - // Handle actual requests - const response = NextResponse.next(); - - if (isAllowedOrigin) { - response.headers.set("Access-Control-Allow-Origin", origin); - response.headers.set("Access-Control-Allow-Credentials", "true"); - } - - return response; -} - -// Apply middleware to all API routes -export const config = { - matcher: "/api/:path*", -};