Spaces:
Sleeping
Sleeping
Commit ·
117459b
1
Parent(s): 42ee0e1
Add missing API endpoints for contributor, messaging, and RAG
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- scripts/migrate-data.ts +949 -0
- src/app/api/ai/chat/route.ts +41 -0
- src/app/api/ai/mentor-match/route.ts +32 -0
- src/app/api/ai/rag/route.ts +36 -0
- src/app/api/ai/triage/route.ts +41 -0
- src/app/api/auth/github/callback/route.ts +101 -0
- src/app/api/auth/github/route.ts +15 -0
- src/app/api/auth/me/route.ts +22 -0
- src/app/api/auth/select-role/route.ts +51 -0
- src/app/api/chat/route.ts +43 -0
- src/app/api/contributor/claim-activity/[issueId]/route.ts +32 -0
- src/app/api/contributor/claim-issue/[issueId]/route.ts +32 -0
- src/app/api/contributor/claim-issue/route.ts +38 -0
- src/app/api/contributor/dashboard-summary/route.ts +44 -0
- src/app/api/contributor/my-claimed-issues/route.ts +27 -0
- src/app/api/contributor/my-issues/route.ts +36 -0
- src/app/api/contributor/route.ts +54 -0
- src/app/api/issues/[id]/messages/route.ts +139 -0
- src/app/api/issues/route.ts +142 -0
- src/app/api/maintainer/dashboard-summary/route.ts +39 -0
- src/app/api/maintainer/issues/route.ts +44 -0
- src/app/api/maintainer/route.ts +49 -0
- src/app/api/maintainer/templates/route.ts +25 -0
- src/app/api/messages/route.ts +63 -0
- src/app/api/messaging/conversations/route.ts +36 -0
- src/app/api/messaging/history/[userId]/route.ts +35 -0
- src/app/api/messaging/mark-read/[userId]/route.ts +31 -0
- src/app/api/messaging/poll/[userId]/route.ts +33 -0
- src/app/api/messaging/route.ts +34 -0
- src/app/api/messaging/send/route.ts +40 -0
- src/app/api/messaging/unread-count/route.ts +26 -0
- src/app/api/profile/[id]/connected-repos/route.ts +23 -0
- src/app/api/profile/[username]/featured-badges/route.ts +20 -0
- src/app/api/profile/[username]/repos/route.ts +31 -0
- src/app/api/profile/[username]/route.ts +19 -0
- src/app/api/profile/route.ts +59 -0
- src/app/api/rag/chat/route.ts +37 -0
- src/app/api/rag/index/route.ts +37 -0
- src/app/api/rag/search/route.ts +37 -0
- src/app/api/rag/suggestions/route.ts +40 -0
- src/app/api/repositories/contributor/route.ts +98 -0
- src/app/api/repositories/route.ts +122 -0
- src/app/api/spark/badges/user/[username]/route.ts +16 -0
- src/app/api/spark/gamification/calendar/[username]/route.ts +16 -0
- src/app/api/spark/gamification/streak/[username]/route.ts +16 -0
- src/app/api/triage/route.ts +181 -0
- src/app/favicon.ico +0 -0
- src/app/globals.css +26 -0
- src/app/layout.tsx +34 -0
- src/app/page.tsx +65 -0
scripts/migrate-data.ts
ADDED
|
@@ -0,0 +1,949 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* MongoDB to Turso Data Migration Script
|
| 3 |
+
*
|
| 4 |
+
* Migrates all data from MongoDB Atlas to Turso (SQLite).
|
| 5 |
+
* MongoDB remains READ-ONLY - no deletions or modifications.
|
| 6 |
+
*
|
| 7 |
+
* Usage:
|
| 8 |
+
* npm run migrate:data
|
| 9 |
+
* npm run migrate:data -- --dry-run
|
| 10 |
+
*
|
| 11 |
+
* Required env vars:
|
| 12 |
+
* MONGO_URL, TURSO_DATABASE_URL, TURSO_AUTH_TOKEN
|
| 13 |
+
*/
|
| 14 |
+
|
| 15 |
+
import { MongoClient, ObjectId } from "mongodb";
|
| 16 |
+
import { createClient } from "@libsql/client";
|
| 17 |
+
import { drizzle } from "drizzle-orm/libsql";
|
| 18 |
+
import { sql } from "drizzle-orm";
|
| 19 |
+
import * as schema from "../src/db/schema";
|
| 20 |
+
import { v4 as uuidv4 } from "uuid";
|
| 21 |
+
import * as dotenv from "dotenv";
|
| 22 |
+
|
| 23 |
+
dotenv.config({ path: ".env.local" });
|
| 24 |
+
|
| 25 |
+
// =============================================================================
|
| 26 |
+
// Types
|
| 27 |
+
// =============================================================================
|
| 28 |
+
|
| 29 |
+
interface MongoUser {
|
| 30 |
+
_id?: ObjectId;
|
| 31 |
+
id?: string;
|
| 32 |
+
githubId: number;
|
| 33 |
+
username: string;
|
| 34 |
+
avatarUrl: string;
|
| 35 |
+
role?: string;
|
| 36 |
+
repositories?: string[];
|
| 37 |
+
githubAccessToken?: string;
|
| 38 |
+
createdAt?: Date | string;
|
| 39 |
+
updatedAt?: Date | string;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
interface MongoRepository {
|
| 43 |
+
_id?: ObjectId;
|
| 44 |
+
id?: string;
|
| 45 |
+
githubRepoId: number;
|
| 46 |
+
name: string;
|
| 47 |
+
owner: string;
|
| 48 |
+
userId: string;
|
| 49 |
+
createdAt?: Date | string;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
interface MongoIssue {
|
| 53 |
+
_id?: ObjectId;
|
| 54 |
+
id?: string;
|
| 55 |
+
githubIssueId: number;
|
| 56 |
+
number: number;
|
| 57 |
+
title: string;
|
| 58 |
+
body?: string;
|
| 59 |
+
authorName: string;
|
| 60 |
+
repoId: string;
|
| 61 |
+
repoName: string;
|
| 62 |
+
owner?: string;
|
| 63 |
+
repo?: string;
|
| 64 |
+
htmlUrl?: string;
|
| 65 |
+
state?: string;
|
| 66 |
+
isPR?: boolean;
|
| 67 |
+
createdAt?: Date | string;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
interface MongoMessage {
|
| 71 |
+
_id?: ObjectId;
|
| 72 |
+
id?: string;
|
| 73 |
+
senderId: string;
|
| 74 |
+
receiverId: string;
|
| 75 |
+
content: string;
|
| 76 |
+
read?: boolean;
|
| 77 |
+
timestamp?: Date | string;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
interface MongoProfile {
|
| 81 |
+
_id?: ObjectId;
|
| 82 |
+
user_id: string;
|
| 83 |
+
username: string;
|
| 84 |
+
avatar_url?: string;
|
| 85 |
+
bio?: string;
|
| 86 |
+
skills?: string[];
|
| 87 |
+
location?: string;
|
| 88 |
+
website?: string;
|
| 89 |
+
twitter?: string;
|
| 90 |
+
available_for_mentoring?: boolean;
|
| 91 |
+
mentoring_topics?: string[];
|
| 92 |
+
connected_repos?: string[];
|
| 93 |
+
profile_visibility?: string;
|
| 94 |
+
show_email?: boolean;
|
| 95 |
+
github_stats?: object;
|
| 96 |
+
stats_updated_at?: Date | string;
|
| 97 |
+
created_at?: Date | string;
|
| 98 |
+
updated_at?: Date | string;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
interface MongoTriageData {
|
| 102 |
+
_id?: ObjectId;
|
| 103 |
+
id?: string;
|
| 104 |
+
issueId: string;
|
| 105 |
+
classification: string;
|
| 106 |
+
summary: string;
|
| 107 |
+
suggestedLabel: string;
|
| 108 |
+
sentiment: string;
|
| 109 |
+
analyzedAt?: Date | string;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
interface MongoTemplate {
|
| 113 |
+
_id?: ObjectId;
|
| 114 |
+
id?: string;
|
| 115 |
+
name: string;
|
| 116 |
+
body: string;
|
| 117 |
+
ownerId: string;
|
| 118 |
+
triggerClassification?: string;
|
| 119 |
+
createdAt?: Date | string;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
interface MongoChatMessage {
|
| 123 |
+
role: string;
|
| 124 |
+
content: string;
|
| 125 |
+
timestamp?: Date | string;
|
| 126 |
+
githubCommentId?: string;
|
| 127 |
+
githubCommentUrl?: string;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
interface MongoChatHistory {
|
| 131 |
+
_id?: ObjectId;
|
| 132 |
+
id?: string;
|
| 133 |
+
userId: string;
|
| 134 |
+
sessionId: string;
|
| 135 |
+
messages?: MongoChatMessage[];
|
| 136 |
+
createdAt?: Date | string;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
interface MongoMentor {
|
| 140 |
+
_id?: ObjectId;
|
| 141 |
+
id?: string;
|
| 142 |
+
userId: string;
|
| 143 |
+
username: string;
|
| 144 |
+
expertiseLevel?: string;
|
| 145 |
+
availabilityHoursPerWeek?: number;
|
| 146 |
+
timezone?: string;
|
| 147 |
+
isActive?: boolean;
|
| 148 |
+
bio?: string;
|
| 149 |
+
avatarUrl?: string;
|
| 150 |
+
techStack?: string[];
|
| 151 |
+
languages?: string[];
|
| 152 |
+
frameworks?: string[];
|
| 153 |
+
preferredTopics?: string[];
|
| 154 |
+
menteeCount?: number;
|
| 155 |
+
sessionsCompleted?: number;
|
| 156 |
+
avgRating?: number;
|
| 157 |
+
totalRatings?: number;
|
| 158 |
+
maxMentees?: number;
|
| 159 |
+
createdAt?: Date | string;
|
| 160 |
+
updatedAt?: Date | string;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
interface MongoTrophy {
|
| 164 |
+
_id?: ObjectId;
|
| 165 |
+
id?: string;
|
| 166 |
+
userId: string;
|
| 167 |
+
username: string;
|
| 168 |
+
trophyType: string;
|
| 169 |
+
name: string;
|
| 170 |
+
description: string;
|
| 171 |
+
icon: string;
|
| 172 |
+
color: string;
|
| 173 |
+
rarity: string;
|
| 174 |
+
svgData?: string;
|
| 175 |
+
isPublic?: boolean;
|
| 176 |
+
shareUrl?: string;
|
| 177 |
+
earnedFor?: string;
|
| 178 |
+
milestoneValue?: number;
|
| 179 |
+
awardedAt?: Date | string;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
interface MongoIssueChat {
|
| 183 |
+
_id?: ObjectId;
|
| 184 |
+
id?: string;
|
| 185 |
+
issueId: string;
|
| 186 |
+
userId: string;
|
| 187 |
+
sessionId: string;
|
| 188 |
+
messages?: MongoChatMessage[];
|
| 189 |
+
createdAt?: Date | string;
|
| 190 |
+
updatedAt?: Date | string;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
interface MongoResource {
|
| 194 |
+
_id?: ObjectId;
|
| 195 |
+
id?: string;
|
| 196 |
+
repoName: string;
|
| 197 |
+
sourceType?: string;
|
| 198 |
+
sourceId?: string;
|
| 199 |
+
resourceType: string;
|
| 200 |
+
title: string;
|
| 201 |
+
content: string;
|
| 202 |
+
description?: string;
|
| 203 |
+
language?: string;
|
| 204 |
+
sharedBy: string;
|
| 205 |
+
sharedById: string;
|
| 206 |
+
tags?: string[];
|
| 207 |
+
saveCount?: number;
|
| 208 |
+
helpfulCount?: number;
|
| 209 |
+
createdAt?: Date | string;
|
| 210 |
+
updatedAt?: Date | string;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
// =============================================================================
|
| 214 |
+
// Utilities
|
| 215 |
+
// =============================================================================
|
| 216 |
+
|
| 217 |
+
function toIsoString(date: Date | string | undefined): string {
|
| 218 |
+
if (!date) return new Date().toISOString();
|
| 219 |
+
if (typeof date === "string") return date;
|
| 220 |
+
return date.toISOString();
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
function extractId(doc: { _id?: ObjectId; id?: string }): string {
|
| 224 |
+
if (doc.id) return doc.id;
|
| 225 |
+
if (doc._id) return doc._id.toString();
|
| 226 |
+
return uuidv4();
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
function log(message: string, type: "info" | "success" | "error" | "warn" = "info") {
|
| 230 |
+
const icons = { info: "ℹ️", success: "✅", error: "❌", warn: "⚠️" };
|
| 231 |
+
const time = new Date().toISOString().split("T")[1].split(".")[0];
|
| 232 |
+
console.log(`[${time}] ${icons[type]} ${message}`);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
// =============================================================================
|
| 236 |
+
// Migration Functions
|
| 237 |
+
// =============================================================================
|
| 238 |
+
|
| 239 |
+
async function migrateUsers(
|
| 240 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 241 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 242 |
+
isDryRun: boolean
|
| 243 |
+
): Promise<Map<string, string>> {
|
| 244 |
+
log("Migrating Users...");
|
| 245 |
+
const userIdMap = new Map<string, string>();
|
| 246 |
+
const users = await mongoDb.collection<MongoUser>("users").find().toArray();
|
| 247 |
+
log(`Found ${users.length} users`);
|
| 248 |
+
|
| 249 |
+
let success = 0, skipped = 0;
|
| 250 |
+
for (const mongoUser of users) {
|
| 251 |
+
const userId = extractId(mongoUser);
|
| 252 |
+
const originalId = mongoUser._id?.toString() || mongoUser.id || "";
|
| 253 |
+
userIdMap.set(originalId, userId);
|
| 254 |
+
|
| 255 |
+
if (!isDryRun) {
|
| 256 |
+
try {
|
| 257 |
+
await tursoDb.insert(schema.users).values({
|
| 258 |
+
id: userId,
|
| 259 |
+
githubId: mongoUser.githubId,
|
| 260 |
+
username: mongoUser.username,
|
| 261 |
+
avatarUrl: mongoUser.avatarUrl,
|
| 262 |
+
role: mongoUser.role || null,
|
| 263 |
+
githubAccessToken: mongoUser.githubAccessToken || null,
|
| 264 |
+
createdAt: toIsoString(mongoUser.createdAt),
|
| 265 |
+
updatedAt: toIsoString(mongoUser.updatedAt),
|
| 266 |
+
}).onConflictDoNothing();
|
| 267 |
+
|
| 268 |
+
// Migrate user repositories array
|
| 269 |
+
if (mongoUser.repositories?.length) {
|
| 270 |
+
for (const repoName of mongoUser.repositories) {
|
| 271 |
+
await tursoDb.insert(schema.userRepositories).values({
|
| 272 |
+
id: uuidv4(),
|
| 273 |
+
userId: userId,
|
| 274 |
+
repoFullName: repoName,
|
| 275 |
+
addedAt: toIsoString(mongoUser.createdAt),
|
| 276 |
+
}).onConflictDoNothing();
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
success++;
|
| 280 |
+
} catch (e) {
|
| 281 |
+
skipped++;
|
| 282 |
+
}
|
| 283 |
+
} else {
|
| 284 |
+
log(`[DRY] User: ${mongoUser.username}`);
|
| 285 |
+
success++;
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
log(`Users: ${success} migrated, ${skipped} skipped`, "success");
|
| 289 |
+
return userIdMap;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
async function migrateRepositories(
|
| 293 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 294 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 295 |
+
userIdMap: Map<string, string>,
|
| 296 |
+
isDryRun: boolean
|
| 297 |
+
): Promise<Map<string, string>> {
|
| 298 |
+
log("Migrating Repositories...");
|
| 299 |
+
const repoIdMap = new Map<string, string>();
|
| 300 |
+
const repos = await mongoDb.collection<MongoRepository>("repositories").find().toArray();
|
| 301 |
+
log(`Found ${repos.length} repositories`);
|
| 302 |
+
|
| 303 |
+
let success = 0, skipped = 0;
|
| 304 |
+
for (const repo of repos) {
|
| 305 |
+
const repoId = extractId(repo);
|
| 306 |
+
const originalId = repo._id?.toString() || repo.id || "";
|
| 307 |
+
repoIdMap.set(originalId, repoId);
|
| 308 |
+
|
| 309 |
+
const mappedUserId = userIdMap.get(repo.userId) || repo.userId;
|
| 310 |
+
|
| 311 |
+
if (!isDryRun) {
|
| 312 |
+
try {
|
| 313 |
+
await tursoDb.insert(schema.repositories).values({
|
| 314 |
+
id: repoId,
|
| 315 |
+
githubRepoId: repo.githubRepoId,
|
| 316 |
+
name: repo.name,
|
| 317 |
+
owner: repo.owner,
|
| 318 |
+
userId: mappedUserId,
|
| 319 |
+
createdAt: toIsoString(repo.createdAt),
|
| 320 |
+
}).onConflictDoNothing();
|
| 321 |
+
success++;
|
| 322 |
+
} catch (e) {
|
| 323 |
+
skipped++;
|
| 324 |
+
}
|
| 325 |
+
} else {
|
| 326 |
+
log(`[DRY] Repo: ${repo.owner}/${repo.name}`);
|
| 327 |
+
success++;
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
log(`Repositories: ${success} migrated, ${skipped} skipped`, "success");
|
| 331 |
+
return repoIdMap;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
async function migrateIssues(
|
| 335 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 336 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 337 |
+
repoIdMap: Map<string, string>,
|
| 338 |
+
isDryRun: boolean
|
| 339 |
+
): Promise<Map<string, string>> {
|
| 340 |
+
log("Migrating Issues...");
|
| 341 |
+
const issueIdMap = new Map<string, string>();
|
| 342 |
+
const issues = await mongoDb.collection<MongoIssue>("issues").find().toArray();
|
| 343 |
+
log(`Found ${issues.length} issues`);
|
| 344 |
+
|
| 345 |
+
let success = 0, skipped = 0;
|
| 346 |
+
for (const issue of issues) {
|
| 347 |
+
const issueId = extractId(issue);
|
| 348 |
+
const originalId = issue._id?.toString() || issue.id || "";
|
| 349 |
+
issueIdMap.set(originalId, issueId);
|
| 350 |
+
|
| 351 |
+
const mappedRepoId = repoIdMap.get(issue.repoId) || issue.repoId;
|
| 352 |
+
|
| 353 |
+
if (!isDryRun) {
|
| 354 |
+
try {
|
| 355 |
+
await tursoDb.insert(schema.issues).values({
|
| 356 |
+
id: issueId,
|
| 357 |
+
githubIssueId: issue.githubIssueId,
|
| 358 |
+
number: issue.number,
|
| 359 |
+
title: issue.title,
|
| 360 |
+
body: issue.body || null,
|
| 361 |
+
authorName: issue.authorName,
|
| 362 |
+
repoId: mappedRepoId,
|
| 363 |
+
repoName: issue.repoName,
|
| 364 |
+
owner: issue.owner || null,
|
| 365 |
+
repo: issue.repo || null,
|
| 366 |
+
htmlUrl: issue.htmlUrl || null,
|
| 367 |
+
state: issue.state || "open",
|
| 368 |
+
isPR: issue.isPR || false,
|
| 369 |
+
createdAt: toIsoString(issue.createdAt),
|
| 370 |
+
}).onConflictDoNothing();
|
| 371 |
+
success++;
|
| 372 |
+
} catch (e) {
|
| 373 |
+
skipped++;
|
| 374 |
+
}
|
| 375 |
+
} else {
|
| 376 |
+
log(`[DRY] Issue: #${issue.number} - ${issue.title.substring(0, 30)}...`);
|
| 377 |
+
success++;
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
log(`Issues: ${success} migrated, ${skipped} skipped`, "success");
|
| 381 |
+
return issueIdMap;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
async function migrateMessages(
|
| 385 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 386 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 387 |
+
userIdMap: Map<string, string>,
|
| 388 |
+
isDryRun: boolean
|
| 389 |
+
): Promise<void> {
|
| 390 |
+
log("Migrating Messages...");
|
| 391 |
+
const messages = await mongoDb.collection<MongoMessage>("messages").find().toArray();
|
| 392 |
+
log(`Found ${messages.length} messages`);
|
| 393 |
+
|
| 394 |
+
let success = 0, skipped = 0;
|
| 395 |
+
for (const msg of messages) {
|
| 396 |
+
const msgId = extractId(msg);
|
| 397 |
+
const senderId = userIdMap.get(msg.senderId) || msg.senderId;
|
| 398 |
+
const receiverId = userIdMap.get(msg.receiverId) || msg.receiverId;
|
| 399 |
+
|
| 400 |
+
if (!isDryRun) {
|
| 401 |
+
try {
|
| 402 |
+
await tursoDb.insert(schema.messages).values({
|
| 403 |
+
id: msgId,
|
| 404 |
+
senderId: senderId,
|
| 405 |
+
receiverId: receiverId,
|
| 406 |
+
content: msg.content,
|
| 407 |
+
read: msg.read || false,
|
| 408 |
+
timestamp: toIsoString(msg.timestamp),
|
| 409 |
+
}).onConflictDoNothing();
|
| 410 |
+
success++;
|
| 411 |
+
} catch (e) {
|
| 412 |
+
skipped++;
|
| 413 |
+
}
|
| 414 |
+
} else {
|
| 415 |
+
success++;
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
log(`Messages: ${success} migrated, ${skipped} skipped`, "success");
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
async function migrateProfiles(
|
| 422 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 423 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 424 |
+
userIdMap: Map<string, string>,
|
| 425 |
+
isDryRun: boolean
|
| 426 |
+
): Promise<void> {
|
| 427 |
+
log("Migrating Profiles...");
|
| 428 |
+
const profiles = await mongoDb.collection<MongoProfile>("profiles").find().toArray();
|
| 429 |
+
log(`Found ${profiles.length} profiles`);
|
| 430 |
+
|
| 431 |
+
let success = 0, skipped = 0;
|
| 432 |
+
for (const profile of profiles) {
|
| 433 |
+
const userId = userIdMap.get(profile.user_id) || profile.user_id;
|
| 434 |
+
|
| 435 |
+
if (!isDryRun) {
|
| 436 |
+
try {
|
| 437 |
+
await tursoDb.insert(schema.profiles).values({
|
| 438 |
+
userId: userId,
|
| 439 |
+
username: profile.username,
|
| 440 |
+
avatarUrl: profile.avatar_url || null,
|
| 441 |
+
bio: profile.bio || null,
|
| 442 |
+
location: profile.location || null,
|
| 443 |
+
website: profile.website || null,
|
| 444 |
+
twitter: profile.twitter || null,
|
| 445 |
+
availableForMentoring: profile.available_for_mentoring || false,
|
| 446 |
+
profileVisibility: profile.profile_visibility || "public",
|
| 447 |
+
showEmail: profile.show_email || false,
|
| 448 |
+
githubStats: profile.github_stats ? JSON.stringify(profile.github_stats) : null,
|
| 449 |
+
statsUpdatedAt: profile.stats_updated_at ? toIsoString(profile.stats_updated_at) : null,
|
| 450 |
+
createdAt: toIsoString(profile.created_at),
|
| 451 |
+
updatedAt: toIsoString(profile.updated_at),
|
| 452 |
+
}).onConflictDoNothing();
|
| 453 |
+
|
| 454 |
+
// Migrate skills
|
| 455 |
+
if (profile.skills?.length) {
|
| 456 |
+
for (const skill of profile.skills) {
|
| 457 |
+
await tursoDb.insert(schema.profileSkills).values({
|
| 458 |
+
profileId: userId,
|
| 459 |
+
skill: skill,
|
| 460 |
+
}).onConflictDoNothing();
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
// Migrate mentoring topics
|
| 465 |
+
if (profile.mentoring_topics?.length) {
|
| 466 |
+
for (const topic of profile.mentoring_topics) {
|
| 467 |
+
await tursoDb.insert(schema.profileMentoringTopics).values({
|
| 468 |
+
profileId: userId,
|
| 469 |
+
topic: topic,
|
| 470 |
+
}).onConflictDoNothing();
|
| 471 |
+
}
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
// Migrate connected repos
|
| 475 |
+
if (profile.connected_repos?.length) {
|
| 476 |
+
for (const repo of profile.connected_repos) {
|
| 477 |
+
await tursoDb.insert(schema.profileConnectedRepos).values({
|
| 478 |
+
profileId: userId,
|
| 479 |
+
repoName: repo,
|
| 480 |
+
}).onConflictDoNothing();
|
| 481 |
+
}
|
| 482 |
+
}
|
| 483 |
+
success++;
|
| 484 |
+
} catch (e) {
|
| 485 |
+
skipped++;
|
| 486 |
+
}
|
| 487 |
+
} else {
|
| 488 |
+
success++;
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
log(`Profiles: ${success} migrated, ${skipped} skipped`, "success");
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
async function migrateTriageData(
|
| 495 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 496 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 497 |
+
issueIdMap: Map<string, string>,
|
| 498 |
+
isDryRun: boolean
|
| 499 |
+
): Promise<void> {
|
| 500 |
+
log("Migrating Triage Data...");
|
| 501 |
+
const triageData = await mongoDb.collection<MongoTriageData>("triageData").find().toArray();
|
| 502 |
+
log(`Found ${triageData.length} triage records`);
|
| 503 |
+
|
| 504 |
+
let success = 0, skipped = 0;
|
| 505 |
+
for (const triage of triageData) {
|
| 506 |
+
const triageId = extractId(triage);
|
| 507 |
+
const issueId = issueIdMap.get(triage.issueId) || triage.issueId;
|
| 508 |
+
|
| 509 |
+
if (!isDryRun) {
|
| 510 |
+
try {
|
| 511 |
+
await tursoDb.insert(schema.triageData).values({
|
| 512 |
+
id: triageId,
|
| 513 |
+
issueId: issueId,
|
| 514 |
+
classification: triage.classification,
|
| 515 |
+
summary: triage.summary,
|
| 516 |
+
suggestedLabel: triage.suggestedLabel,
|
| 517 |
+
sentiment: triage.sentiment,
|
| 518 |
+
analyzedAt: toIsoString(triage.analyzedAt),
|
| 519 |
+
}).onConflictDoNothing();
|
| 520 |
+
success++;
|
| 521 |
+
} catch (e) {
|
| 522 |
+
skipped++;
|
| 523 |
+
}
|
| 524 |
+
} else {
|
| 525 |
+
success++;
|
| 526 |
+
}
|
| 527 |
+
}
|
| 528 |
+
log(`Triage Data: ${success} migrated, ${skipped} skipped`, "success");
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
async function migrateTemplates(
|
| 532 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 533 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 534 |
+
userIdMap: Map<string, string>,
|
| 535 |
+
isDryRun: boolean
|
| 536 |
+
): Promise<void> {
|
| 537 |
+
log("Migrating Templates...");
|
| 538 |
+
const templates = await mongoDb.collection<MongoTemplate>("templates").find().toArray();
|
| 539 |
+
log(`Found ${templates.length} templates`);
|
| 540 |
+
|
| 541 |
+
let success = 0, skipped = 0;
|
| 542 |
+
for (const template of templates) {
|
| 543 |
+
const templateId = extractId(template);
|
| 544 |
+
const ownerId = userIdMap.get(template.ownerId) || template.ownerId;
|
| 545 |
+
|
| 546 |
+
if (!isDryRun) {
|
| 547 |
+
try {
|
| 548 |
+
await tursoDb.insert(schema.templates).values({
|
| 549 |
+
id: templateId,
|
| 550 |
+
name: template.name,
|
| 551 |
+
body: template.body,
|
| 552 |
+
ownerId: ownerId,
|
| 553 |
+
triggerClassification: template.triggerClassification || null,
|
| 554 |
+
createdAt: toIsoString(template.createdAt),
|
| 555 |
+
}).onConflictDoNothing();
|
| 556 |
+
success++;
|
| 557 |
+
} catch (e) {
|
| 558 |
+
skipped++;
|
| 559 |
+
}
|
| 560 |
+
} else {
|
| 561 |
+
success++;
|
| 562 |
+
}
|
| 563 |
+
}
|
| 564 |
+
log(`Templates: ${success} migrated, ${skipped} skipped`, "success");
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
async function migrateChatHistory(
|
| 568 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 569 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 570 |
+
userIdMap: Map<string, string>,
|
| 571 |
+
isDryRun: boolean
|
| 572 |
+
): Promise<void> {
|
| 573 |
+
log("Migrating Chat History...");
|
| 574 |
+
const chatHistories = await mongoDb.collection<MongoChatHistory>("chat_history").find().toArray();
|
| 575 |
+
log(`Found ${chatHistories.length} chat histories`);
|
| 576 |
+
|
| 577 |
+
let historySuccess = 0, historySkipped = 0;
|
| 578 |
+
let messageSuccess = 0, messageSkipped = 0;
|
| 579 |
+
|
| 580 |
+
for (const history of chatHistories) {
|
| 581 |
+
const historyId = extractId(history);
|
| 582 |
+
const userId = userIdMap.get(history.userId) || history.userId;
|
| 583 |
+
|
| 584 |
+
if (!isDryRun) {
|
| 585 |
+
try {
|
| 586 |
+
// Insert chat history record
|
| 587 |
+
await tursoDb.insert(schema.chatHistory).values({
|
| 588 |
+
id: historyId,
|
| 589 |
+
userId: userId,
|
| 590 |
+
sessionId: history.sessionId,
|
| 591 |
+
createdAt: toIsoString(history.createdAt),
|
| 592 |
+
}).onConflictDoNothing();
|
| 593 |
+
historySuccess++;
|
| 594 |
+
|
| 595 |
+
// Insert related messages
|
| 596 |
+
if (history.messages?.length) {
|
| 597 |
+
for (const msg of history.messages) {
|
| 598 |
+
const msgId = uuidv4();
|
| 599 |
+
try {
|
| 600 |
+
await tursoDb.insert(schema.chatHistoryMessages).values({
|
| 601 |
+
id: msgId,
|
| 602 |
+
chatHistoryId: historyId,
|
| 603 |
+
role: msg.role,
|
| 604 |
+
content: msg.content,
|
| 605 |
+
timestamp: toIsoString(msg.timestamp),
|
| 606 |
+
githubCommentId: msg.githubCommentId || null,
|
| 607 |
+
githubCommentUrl: msg.githubCommentUrl || null,
|
| 608 |
+
}).onConflictDoNothing();
|
| 609 |
+
messageSuccess++;
|
| 610 |
+
} catch (e) {
|
| 611 |
+
messageSkipped++;
|
| 612 |
+
}
|
| 613 |
+
}
|
| 614 |
+
}
|
| 615 |
+
} catch (e) {
|
| 616 |
+
historySkipped++;
|
| 617 |
+
}
|
| 618 |
+
} else {
|
| 619 |
+
log(`[DRY] Chat History: ${historyId} with ${history.messages?.length || 0} messages`);
|
| 620 |
+
historySuccess++;
|
| 621 |
+
messageSuccess += history.messages?.length || 0;
|
| 622 |
+
}
|
| 623 |
+
}
|
| 624 |
+
log(`Chat History: ${historySuccess} migrated, ${historySkipped} skipped`, "success");
|
| 625 |
+
log(`Chat Messages: ${messageSuccess} migrated, ${messageSkipped} skipped`, "success");
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
async function migrateMentors(
|
| 629 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 630 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 631 |
+
userIdMap: Map<string, string>,
|
| 632 |
+
isDryRun: boolean
|
| 633 |
+
): Promise<Map<string, string>> {
|
| 634 |
+
log("Migrating Mentors...");
|
| 635 |
+
const mentorIdMap = new Map<string, string>();
|
| 636 |
+
const mentors = await mongoDb.collection<MongoMentor>("mentors").find().toArray();
|
| 637 |
+
log(`Found ${mentors.length} mentors`);
|
| 638 |
+
|
| 639 |
+
let success = 0, skipped = 0;
|
| 640 |
+
for (const mentor of mentors) {
|
| 641 |
+
const mentorId = extractId(mentor);
|
| 642 |
+
const originalId = mentor._id?.toString() || mentor.id || "";
|
| 643 |
+
mentorIdMap.set(originalId, mentorId);
|
| 644 |
+
|
| 645 |
+
const mappedUserId = userIdMap.get(mentor.userId) || mentor.userId;
|
| 646 |
+
|
| 647 |
+
if (!isDryRun) {
|
| 648 |
+
try {
|
| 649 |
+
await tursoDb.insert(schema.mentors).values({
|
| 650 |
+
id: mentorId,
|
| 651 |
+
userId: mappedUserId,
|
| 652 |
+
username: mentor.username,
|
| 653 |
+
expertiseLevel: mentor.expertiseLevel || "intermediate",
|
| 654 |
+
availabilityHoursPerWeek: mentor.availabilityHoursPerWeek || 5,
|
| 655 |
+
timezone: mentor.timezone || null,
|
| 656 |
+
isActive: mentor.isActive ?? true,
|
| 657 |
+
bio: mentor.bio || null,
|
| 658 |
+
avatarUrl: mentor.avatarUrl || null,
|
| 659 |
+
menteeCount: mentor.menteeCount || 0,
|
| 660 |
+
sessionsCompleted: mentor.sessionsCompleted || 0,
|
| 661 |
+
avgRating: mentor.avgRating || 0,
|
| 662 |
+
totalRatings: mentor.totalRatings || 0,
|
| 663 |
+
maxMentees: mentor.maxMentees || 3,
|
| 664 |
+
createdAt: toIsoString(mentor.createdAt),
|
| 665 |
+
updatedAt: toIsoString(mentor.updatedAt),
|
| 666 |
+
}).onConflictDoNothing();
|
| 667 |
+
|
| 668 |
+
// Migrate tech stack
|
| 669 |
+
if (mentor.techStack?.length) {
|
| 670 |
+
for (const tech of mentor.techStack) {
|
| 671 |
+
await tursoDb.insert(schema.mentorTechStack).values({
|
| 672 |
+
mentorId: mentorId,
|
| 673 |
+
tech: tech,
|
| 674 |
+
}).onConflictDoNothing();
|
| 675 |
+
}
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
// Migrate languages
|
| 679 |
+
if (mentor.languages?.length) {
|
| 680 |
+
for (const lang of mentor.languages) {
|
| 681 |
+
await tursoDb.insert(schema.mentorLanguages).values({
|
| 682 |
+
mentorId: mentorId,
|
| 683 |
+
language: lang,
|
| 684 |
+
}).onConflictDoNothing();
|
| 685 |
+
}
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
// Migrate frameworks
|
| 689 |
+
if (mentor.frameworks?.length) {
|
| 690 |
+
for (const fw of mentor.frameworks) {
|
| 691 |
+
await tursoDb.insert(schema.mentorFrameworks).values({
|
| 692 |
+
mentorId: mentorId,
|
| 693 |
+
framework: fw,
|
| 694 |
+
}).onConflictDoNothing();
|
| 695 |
+
}
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
// Migrate preferred topics
|
| 699 |
+
if (mentor.preferredTopics?.length) {
|
| 700 |
+
for (const topic of mentor.preferredTopics) {
|
| 701 |
+
await tursoDb.insert(schema.mentorPreferredTopics).values({
|
| 702 |
+
mentorId: mentorId,
|
| 703 |
+
topic: topic,
|
| 704 |
+
}).onConflictDoNothing();
|
| 705 |
+
}
|
| 706 |
+
}
|
| 707 |
+
success++;
|
| 708 |
+
} catch (e) {
|
| 709 |
+
skipped++;
|
| 710 |
+
}
|
| 711 |
+
} else {
|
| 712 |
+
log(`[DRY] Mentor: ${mentor.username}`);
|
| 713 |
+
success++;
|
| 714 |
+
}
|
| 715 |
+
}
|
| 716 |
+
log(`Mentors: ${success} migrated, ${skipped} skipped`, "success");
|
| 717 |
+
return mentorIdMap;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
async function migrateTrophies(
|
| 721 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 722 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 723 |
+
userIdMap: Map<string, string>,
|
| 724 |
+
isDryRun: boolean
|
| 725 |
+
): Promise<void> {
|
| 726 |
+
log("Migrating Trophies...");
|
| 727 |
+
const trophies = await mongoDb.collection<MongoTrophy>("trophies").find().toArray();
|
| 728 |
+
log(`Found ${trophies.length} trophies`);
|
| 729 |
+
|
| 730 |
+
let success = 0, skipped = 0;
|
| 731 |
+
for (const trophy of trophies) {
|
| 732 |
+
const trophyId = extractId(trophy);
|
| 733 |
+
const mappedUserId = userIdMap.get(trophy.userId) || trophy.userId;
|
| 734 |
+
|
| 735 |
+
if (!isDryRun) {
|
| 736 |
+
try {
|
| 737 |
+
await tursoDb.insert(schema.trophies).values({
|
| 738 |
+
id: trophyId,
|
| 739 |
+
userId: mappedUserId,
|
| 740 |
+
username: trophy.username,
|
| 741 |
+
trophyType: trophy.trophyType,
|
| 742 |
+
name: trophy.name,
|
| 743 |
+
description: trophy.description,
|
| 744 |
+
icon: trophy.icon,
|
| 745 |
+
color: trophy.color,
|
| 746 |
+
rarity: trophy.rarity,
|
| 747 |
+
svgData: trophy.svgData || null,
|
| 748 |
+
isPublic: trophy.isPublic ?? true,
|
| 749 |
+
shareUrl: trophy.shareUrl || null,
|
| 750 |
+
earnedFor: trophy.earnedFor || null,
|
| 751 |
+
milestoneValue: trophy.milestoneValue || null,
|
| 752 |
+
awardedAt: toIsoString(trophy.awardedAt),
|
| 753 |
+
}).onConflictDoNothing();
|
| 754 |
+
success++;
|
| 755 |
+
} catch (e) {
|
| 756 |
+
skipped++;
|
| 757 |
+
}
|
| 758 |
+
} else {
|
| 759 |
+
log(`[DRY] Trophy: ${trophy.name} for ${trophy.username}`);
|
| 760 |
+
success++;
|
| 761 |
+
}
|
| 762 |
+
}
|
| 763 |
+
log(`Trophies: ${success} migrated, ${skipped} skipped`, "success");
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
async function migrateIssueChats(
|
| 767 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 768 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 769 |
+
userIdMap: Map<string, string>,
|
| 770 |
+
issueIdMap: Map<string, string>,
|
| 771 |
+
isDryRun: boolean
|
| 772 |
+
): Promise<void> {
|
| 773 |
+
log("Migrating Issue Chats...");
|
| 774 |
+
const issueChats = await mongoDb.collection<MongoIssueChat>("issue_chats").find().toArray();
|
| 775 |
+
log(`Found ${issueChats.length} issue chats`);
|
| 776 |
+
|
| 777 |
+
let chatSuccess = 0, chatSkipped = 0;
|
| 778 |
+
let msgSuccess = 0, msgSkipped = 0;
|
| 779 |
+
|
| 780 |
+
for (const chat of issueChats) {
|
| 781 |
+
const chatId = extractId(chat);
|
| 782 |
+
const mappedUserId = userIdMap.get(chat.userId) || chat.userId;
|
| 783 |
+
const mappedIssueId = issueIdMap.get(chat.issueId) || chat.issueId;
|
| 784 |
+
|
| 785 |
+
if (!isDryRun) {
|
| 786 |
+
try {
|
| 787 |
+
await tursoDb.insert(schema.issueChats).values({
|
| 788 |
+
id: chatId,
|
| 789 |
+
issueId: mappedIssueId,
|
| 790 |
+
userId: mappedUserId,
|
| 791 |
+
sessionId: chat.sessionId,
|
| 792 |
+
createdAt: toIsoString(chat.createdAt),
|
| 793 |
+
updatedAt: toIsoString(chat.updatedAt),
|
| 794 |
+
}).onConflictDoNothing();
|
| 795 |
+
chatSuccess++;
|
| 796 |
+
|
| 797 |
+
// Migrate messages
|
| 798 |
+
if (chat.messages?.length) {
|
| 799 |
+
for (const msg of chat.messages) {
|
| 800 |
+
const msgId = uuidv4();
|
| 801 |
+
try {
|
| 802 |
+
await tursoDb.insert(schema.issueChatMessages).values({
|
| 803 |
+
id: msgId,
|
| 804 |
+
issueChatId: chatId,
|
| 805 |
+
role: msg.role,
|
| 806 |
+
content: msg.content,
|
| 807 |
+
timestamp: toIsoString(msg.timestamp),
|
| 808 |
+
githubCommentId: msg.githubCommentId || null,
|
| 809 |
+
githubCommentUrl: msg.githubCommentUrl || null,
|
| 810 |
+
}).onConflictDoNothing();
|
| 811 |
+
msgSuccess++;
|
| 812 |
+
} catch (e) {
|
| 813 |
+
msgSkipped++;
|
| 814 |
+
}
|
| 815 |
+
}
|
| 816 |
+
}
|
| 817 |
+
} catch (e) {
|
| 818 |
+
chatSkipped++;
|
| 819 |
+
}
|
| 820 |
+
} else {
|
| 821 |
+
log(`[DRY] Issue Chat: ${chatId} with ${chat.messages?.length || 0} messages`);
|
| 822 |
+
chatSuccess++;
|
| 823 |
+
msgSuccess += chat.messages?.length || 0;
|
| 824 |
+
}
|
| 825 |
+
}
|
| 826 |
+
log(`Issue Chats: ${chatSuccess} migrated, ${chatSkipped} skipped`, "success");
|
| 827 |
+
log(`Issue Chat Messages: ${msgSuccess} migrated, ${msgSkipped} skipped`, "success");
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
async function migrateResources(
|
| 831 |
+
mongoDb: ReturnType<MongoClient["db"]>,
|
| 832 |
+
tursoDb: ReturnType<typeof drizzle>,
|
| 833 |
+
userIdMap: Map<string, string>,
|
| 834 |
+
isDryRun: boolean
|
| 835 |
+
): Promise<void> {
|
| 836 |
+
log("Migrating Resources...");
|
| 837 |
+
const resources = await mongoDb.collection<MongoResource>("resources").find().toArray();
|
| 838 |
+
log(`Found ${resources.length} resources`);
|
| 839 |
+
|
| 840 |
+
let success = 0, skipped = 0;
|
| 841 |
+
for (const resource of resources) {
|
| 842 |
+
const resourceId = extractId(resource);
|
| 843 |
+
const mappedUserId = userIdMap.get(resource.sharedById) || resource.sharedById;
|
| 844 |
+
|
| 845 |
+
if (!isDryRun) {
|
| 846 |
+
try {
|
| 847 |
+
await tursoDb.insert(schema.resources).values({
|
| 848 |
+
id: resourceId,
|
| 849 |
+
repoName: resource.repoName,
|
| 850 |
+
sourceType: resource.sourceType || "chat",
|
| 851 |
+
sourceId: resource.sourceId || null,
|
| 852 |
+
resourceType: resource.resourceType,
|
| 853 |
+
title: resource.title,
|
| 854 |
+
content: resource.content,
|
| 855 |
+
description: resource.description || null,
|
| 856 |
+
language: resource.language || null,
|
| 857 |
+
sharedBy: resource.sharedBy,
|
| 858 |
+
sharedById: mappedUserId,
|
| 859 |
+
saveCount: resource.saveCount || 0,
|
| 860 |
+
helpfulCount: resource.helpfulCount || 0,
|
| 861 |
+
createdAt: toIsoString(resource.createdAt),
|
| 862 |
+
updatedAt: toIsoString(resource.updatedAt),
|
| 863 |
+
}).onConflictDoNothing();
|
| 864 |
+
|
| 865 |
+
// Migrate tags
|
| 866 |
+
if (resource.tags?.length) {
|
| 867 |
+
for (const tag of resource.tags) {
|
| 868 |
+
await tursoDb.insert(schema.resourceTags).values({
|
| 869 |
+
resourceId: resourceId,
|
| 870 |
+
tag: tag,
|
| 871 |
+
}).onConflictDoNothing();
|
| 872 |
+
}
|
| 873 |
+
}
|
| 874 |
+
success++;
|
| 875 |
+
} catch (e) {
|
| 876 |
+
skipped++;
|
| 877 |
+
}
|
| 878 |
+
} else {
|
| 879 |
+
log(`[DRY] Resource: ${resource.title}`);
|
| 880 |
+
success++;
|
| 881 |
+
}
|
| 882 |
+
}
|
| 883 |
+
log(`Resources: ${success} migrated, ${skipped} skipped`, "success");
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
// =============================================================================
|
| 887 |
+
// Main
|
| 888 |
+
// =============================================================================
|
| 889 |
+
|
| 890 |
+
async function main() {
|
| 891 |
+
const args = process.argv.slice(2);
|
| 892 |
+
const isDryRun = args.includes("--dry-run");
|
| 893 |
+
|
| 894 |
+
if (isDryRun) {
|
| 895 |
+
log("=== DRY RUN MODE ===", "warn");
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
const mongoUri = process.env.MONGO_URL || process.env.MONGODB_URI;
|
| 899 |
+
const tursoUrl = process.env.TURSO_DATABASE_URL;
|
| 900 |
+
const tursoToken = process.env.TURSO_AUTH_TOKEN;
|
| 901 |
+
|
| 902 |
+
if (!mongoUri) {
|
| 903 |
+
log("Missing MONGO_URL or MONGODB_URI", "error");
|
| 904 |
+
process.exit(1);
|
| 905 |
+
}
|
| 906 |
+
if (!tursoUrl) {
|
| 907 |
+
log("Missing TURSO_DATABASE_URL", "error");
|
| 908 |
+
process.exit(1);
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
log("Connecting to MongoDB...");
|
| 912 |
+
const mongoClient = new MongoClient(mongoUri);
|
| 913 |
+
await mongoClient.connect();
|
| 914 |
+
const dbName = process.env.DB_NAME || "opentriage_db";
|
| 915 |
+
const mongoDb = mongoClient.db(dbName);
|
| 916 |
+
log(`Connected to MongoDB (${dbName})`, "success");
|
| 917 |
+
|
| 918 |
+
log("Connecting to Turso...");
|
| 919 |
+
const tursoClient = createClient({ url: tursoUrl, authToken: tursoToken });
|
| 920 |
+
const tursoDb = drizzle(tursoClient, { schema });
|
| 921 |
+
log("Connected to Turso", "success");
|
| 922 |
+
|
| 923 |
+
try {
|
| 924 |
+
// Migrate in order (respecting foreign keys)
|
| 925 |
+
const userIdMap = await migrateUsers(mongoDb, tursoDb, isDryRun);
|
| 926 |
+
const repoIdMap = await migrateRepositories(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 927 |
+
const issueIdMap = await migrateIssues(mongoDb, tursoDb, repoIdMap, isDryRun);
|
| 928 |
+
await migrateMessages(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 929 |
+
await migrateProfiles(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 930 |
+
await migrateTriageData(mongoDb, tursoDb, issueIdMap, isDryRun);
|
| 931 |
+
await migrateTemplates(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 932 |
+
await migrateChatHistory(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 933 |
+
|
| 934 |
+
// New migrations for complete feature support
|
| 935 |
+
const mentorIdMap = await migrateMentors(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 936 |
+
await migrateTrophies(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 937 |
+
await migrateIssueChats(mongoDb, tursoDb, userIdMap, issueIdMap, isDryRun);
|
| 938 |
+
await migrateResources(mongoDb, tursoDb, userIdMap, isDryRun);
|
| 939 |
+
|
| 940 |
+
log("=== Migration Complete ===", "success");
|
| 941 |
+
if (isDryRun) {
|
| 942 |
+
log("Run without --dry-run to perform actual migration", "info");
|
| 943 |
+
}
|
| 944 |
+
} finally {
|
| 945 |
+
await mongoClient.close();
|
| 946 |
+
}
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
main().catch(console.error);
|
src/app/api/ai/chat/route.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* AI Chat Proxy Route
|
| 3 |
+
*
|
| 4 |
+
* Forwards requests to AI_ENGINE_URL/chat
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 8 |
+
import { chat } from "@/lib/ai-client";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
|
| 11 |
+
export async function POST(request: NextRequest) {
|
| 12 |
+
try {
|
| 13 |
+
const user = await getCurrentUser(request);
|
| 14 |
+
if (!user) {
|
| 15 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const body = await request.json();
|
| 19 |
+
const { message, history, context } = body;
|
| 20 |
+
|
| 21 |
+
if (!message) {
|
| 22 |
+
return NextResponse.json({ error: "Message is required" }, { status: 400 });
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const result = await chat(message, history, {
|
| 26 |
+
...context,
|
| 27 |
+
userId: user.id,
|
| 28 |
+
username: user.username,
|
| 29 |
+
role: user.role,
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
if (!result.success) {
|
| 33 |
+
return NextResponse.json({ error: result.error }, { status: 502 });
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
return NextResponse.json(result.data);
|
| 37 |
+
} catch (error) {
|
| 38 |
+
console.error("AI Chat error:", error);
|
| 39 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 40 |
+
}
|
| 41 |
+
}
|
src/app/api/ai/mentor-match/route.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* AI Mentor Match Proxy Route
|
| 3 |
+
*
|
| 4 |
+
* Forwards requests to AI_ENGINE_URL/mentor-match
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 8 |
+
import { findMentorMatches } from "@/lib/ai-client";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
|
| 11 |
+
export async function POST(request: NextRequest) {
|
| 12 |
+
try {
|
| 13 |
+
const user = await getCurrentUser(request);
|
| 14 |
+
if (!user) {
|
| 15 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const body = await request.json();
|
| 19 |
+
const { limit = 5 } = body;
|
| 20 |
+
|
| 21 |
+
const result = await findMentorMatches(user.id, user.username, limit);
|
| 22 |
+
|
| 23 |
+
if (!result.success) {
|
| 24 |
+
return NextResponse.json({ error: result.error }, { status: 502 });
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
return NextResponse.json(result.data);
|
| 28 |
+
} catch (error) {
|
| 29 |
+
console.error("AI Mentor Match error:", error);
|
| 30 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 31 |
+
}
|
| 32 |
+
}
|
src/app/api/ai/rag/route.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* AI RAG Proxy Route
|
| 3 |
+
*
|
| 4 |
+
* Forwards requests to AI_ENGINE_URL/rag/chat
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 8 |
+
import { ragQuery } from "@/lib/ai-client";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
|
| 11 |
+
export async function POST(request: NextRequest) {
|
| 12 |
+
try {
|
| 13 |
+
const user = await getCurrentUser(request);
|
| 14 |
+
if (!user) {
|
| 15 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const body = await request.json();
|
| 19 |
+
const { question, repoName } = body;
|
| 20 |
+
|
| 21 |
+
if (!question) {
|
| 22 |
+
return NextResponse.json({ error: "Question is required" }, { status: 400 });
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const result = await ragQuery(question, repoName);
|
| 26 |
+
|
| 27 |
+
if (!result.success) {
|
| 28 |
+
return NextResponse.json({ error: result.error }, { status: 502 });
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
return NextResponse.json(result.data);
|
| 32 |
+
} catch (error) {
|
| 33 |
+
console.error("AI RAG error:", error);
|
| 34 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 35 |
+
}
|
| 36 |
+
}
|
src/app/api/ai/triage/route.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* AI Triage Proxy Route
|
| 3 |
+
*
|
| 4 |
+
* Forwards requests to AI_ENGINE_URL/triage
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 8 |
+
import { triageIssue } from "@/lib/ai-client";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
|
| 11 |
+
export async function POST(request: NextRequest) {
|
| 12 |
+
try {
|
| 13 |
+
const user = await getCurrentUser(request);
|
| 14 |
+
if (!user) {
|
| 15 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const body = await request.json();
|
| 19 |
+
const { title, body: issueBody, authorName, isPR } = body;
|
| 20 |
+
|
| 21 |
+
if (!title) {
|
| 22 |
+
return NextResponse.json({ error: "Title is required" }, { status: 400 });
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const result = await triageIssue({
|
| 26 |
+
title,
|
| 27 |
+
body: issueBody,
|
| 28 |
+
authorName: authorName || "unknown",
|
| 29 |
+
isPR: isPR || false,
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
if (!result.success) {
|
| 33 |
+
return NextResponse.json({ error: result.error }, { status: 502 });
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
return NextResponse.json(result.data);
|
| 37 |
+
} catch (error) {
|
| 38 |
+
console.error("AI Triage error:", error);
|
| 39 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 40 |
+
}
|
| 41 |
+
}
|
src/app/api/auth/github/callback/route.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { db } from "@/db";
|
| 3 |
+
import { users } from "@/db/schema";
|
| 4 |
+
import { createJwtToken } from "@/lib/auth";
|
| 5 |
+
import { generateId, now } from "@/lib/utils";
|
| 6 |
+
import { eq } from "drizzle-orm";
|
| 7 |
+
|
| 8 |
+
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID!;
|
| 9 |
+
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET!;
|
| 10 |
+
const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:5173";
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* GET /api/auth/github/callback
|
| 14 |
+
* Handle GitHub OAuth callback and create user session.
|
| 15 |
+
*/
|
| 16 |
+
export async function GET(request: NextRequest) {
|
| 17 |
+
const { searchParams } = new URL(request.url);
|
| 18 |
+
const code = searchParams.get("code");
|
| 19 |
+
|
| 20 |
+
if (!code) {
|
| 21 |
+
return NextResponse.redirect(`${FRONTEND_URL}/?error=no_code`);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
try {
|
| 25 |
+
// Exchange code for access token
|
| 26 |
+
const tokenResponse = await fetch(
|
| 27 |
+
"https://github.com/login/oauth/access_token",
|
| 28 |
+
{
|
| 29 |
+
method: "POST",
|
| 30 |
+
headers: {
|
| 31 |
+
Accept: "application/json",
|
| 32 |
+
"Content-Type": "application/json",
|
| 33 |
+
},
|
| 34 |
+
body: JSON.stringify({
|
| 35 |
+
client_id: GITHUB_CLIENT_ID,
|
| 36 |
+
client_secret: GITHUB_CLIENT_SECRET,
|
| 37 |
+
code,
|
| 38 |
+
}),
|
| 39 |
+
}
|
| 40 |
+
);
|
| 41 |
+
|
| 42 |
+
const tokenData = await tokenResponse.json();
|
| 43 |
+
const accessToken = tokenData.access_token;
|
| 44 |
+
|
| 45 |
+
if (!accessToken) {
|
| 46 |
+
console.error("No access token:", tokenData);
|
| 47 |
+
return NextResponse.redirect(`${FRONTEND_URL}/?error=no_token`);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// Get user info from GitHub
|
| 51 |
+
const userResponse = await fetch("https://api.github.com/user", {
|
| 52 |
+
headers: {
|
| 53 |
+
Authorization: `Bearer ${accessToken}`,
|
| 54 |
+
},
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
const githubUser = await userResponse.json();
|
| 58 |
+
|
| 59 |
+
// Check if user exists
|
| 60 |
+
const existingUsers = await db
|
| 61 |
+
.select()
|
| 62 |
+
.from(users)
|
| 63 |
+
.where(eq(users.githubId, githubUser.id))
|
| 64 |
+
.limit(1);
|
| 65 |
+
|
| 66 |
+
let userData;
|
| 67 |
+
|
| 68 |
+
if (existingUsers.length > 0) {
|
| 69 |
+
// Update existing user with new GitHub token
|
| 70 |
+
await db
|
| 71 |
+
.update(users)
|
| 72 |
+
.set({ githubAccessToken: accessToken, updatedAt: now() })
|
| 73 |
+
.where(eq(users.githubId, githubUser.id));
|
| 74 |
+
|
| 75 |
+
userData = { ...existingUsers[0], githubAccessToken: accessToken };
|
| 76 |
+
} else {
|
| 77 |
+
// Create new user
|
| 78 |
+
const newUser = {
|
| 79 |
+
id: generateId(),
|
| 80 |
+
githubId: githubUser.id,
|
| 81 |
+
username: githubUser.login,
|
| 82 |
+
avatarUrl: githubUser.avatar_url,
|
| 83 |
+
role: null,
|
| 84 |
+
githubAccessToken: accessToken,
|
| 85 |
+
createdAt: now(),
|
| 86 |
+
updatedAt: now(),
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
await db.insert(users).values(newUser);
|
| 90 |
+
userData = newUser;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// Create JWT token
|
| 94 |
+
const token = createJwtToken(userData.id, userData.role);
|
| 95 |
+
|
| 96 |
+
return NextResponse.redirect(`${FRONTEND_URL}/?token=${token}`);
|
| 97 |
+
} catch (error) {
|
| 98 |
+
console.error("GitHub auth error:", error);
|
| 99 |
+
return NextResponse.redirect(`${FRONTEND_URL}/?error=auth_failed`);
|
| 100 |
+
}
|
| 101 |
+
}
|
src/app/api/auth/github/route.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID!;
|
| 4 |
+
const API_URL = process.env.API_URL || "http://localhost:3000";
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* GET /api/auth/github
|
| 8 |
+
* Redirect to GitHub OAuth authorization page.
|
| 9 |
+
*/
|
| 10 |
+
export async function GET(request: NextRequest) {
|
| 11 |
+
const callbackUrl = `${API_URL}/api/auth/github/callback`;
|
| 12 |
+
const githubUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&redirect_uri=${callbackUrl}&scope=user:email,repo`;
|
| 13 |
+
|
| 14 |
+
return NextResponse.redirect(githubUrl);
|
| 15 |
+
}
|
src/app/api/auth/me/route.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { requireAuth } from "@/lib/auth";
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* GET /api/auth/me
|
| 6 |
+
* Get current authenticated user information.
|
| 7 |
+
*/
|
| 8 |
+
export async function GET(request: NextRequest) {
|
| 9 |
+
const { user, error } = await requireAuth(request);
|
| 10 |
+
|
| 11 |
+
if (error) {
|
| 12 |
+
return error;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
return NextResponse.json({
|
| 16 |
+
id: user!.id,
|
| 17 |
+
username: user!.username,
|
| 18 |
+
avatarUrl: user!.avatarUrl,
|
| 19 |
+
role: user!.role,
|
| 20 |
+
githubId: user!.githubId,
|
| 21 |
+
});
|
| 22 |
+
}
|
src/app/api/auth/select-role/route.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Role Selection Route
|
| 3 |
+
*
|
| 4 |
+
* POST /api/auth/select-role
|
| 5 |
+
* Allows authenticated users to select their role (MAINTAINER or CONTRIBUTOR)
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { requireAuth, createJwtToken } from "@/lib/auth";
|
| 10 |
+
import { updateUserRole } from "@/lib/db/queries/users";
|
| 11 |
+
|
| 12 |
+
export async function POST(request: NextRequest) {
|
| 13 |
+
const { user, error } = await requireAuth(request);
|
| 14 |
+
|
| 15 |
+
if (error) {
|
| 16 |
+
return error;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
try {
|
| 20 |
+
const body = await request.json();
|
| 21 |
+
const { role } = body;
|
| 22 |
+
|
| 23 |
+
// Validate role
|
| 24 |
+
if (!role || !["MAINTAINER", "CONTRIBUTOR"].includes(role.toUpperCase())) {
|
| 25 |
+
return NextResponse.json(
|
| 26 |
+
{ error: "Invalid role. Must be MAINTAINER or CONTRIBUTOR" },
|
| 27 |
+
{ status: 400 }
|
| 28 |
+
);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const normalizedRole = role.toUpperCase();
|
| 32 |
+
|
| 33 |
+
// Update user role in database
|
| 34 |
+
await updateUserRole(user!.id, normalizedRole);
|
| 35 |
+
|
| 36 |
+
// Generate new token with updated role
|
| 37 |
+
const newToken = createJwtToken(user!.id, normalizedRole);
|
| 38 |
+
|
| 39 |
+
return NextResponse.json({
|
| 40 |
+
success: true,
|
| 41 |
+
role: normalizedRole,
|
| 42 |
+
token: newToken,
|
| 43 |
+
});
|
| 44 |
+
} catch (error) {
|
| 45 |
+
console.error("Role selection error:", error);
|
| 46 |
+
return NextResponse.json(
|
| 47 |
+
{ error: "Failed to update role" },
|
| 48 |
+
{ status: 500 }
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
}
|
src/app/api/chat/route.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Chat Route
|
| 3 |
+
*
|
| 4 |
+
* POST /api/chat
|
| 5 |
+
* Proxy to /api/ai/chat for convenience
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { chat } from "@/lib/ai-client";
|
| 10 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 11 |
+
|
| 12 |
+
export async function POST(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const body = await request.json();
|
| 20 |
+
const { message, sessionId, history, context } = body;
|
| 21 |
+
|
| 22 |
+
if (!message) {
|
| 23 |
+
return NextResponse.json({ error: "Message is required" }, { status: 400 });
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const result = await chat(message, history, {
|
| 27 |
+
...context,
|
| 28 |
+
sessionId,
|
| 29 |
+
userId: user.id,
|
| 30 |
+
username: user.username,
|
| 31 |
+
role: user.role,
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
if (!result.success) {
|
| 35 |
+
return NextResponse.json({ error: result.error }, { status: 502 });
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
return NextResponse.json(result.data);
|
| 39 |
+
} catch (error) {
|
| 40 |
+
console.error("Chat error:", error);
|
| 41 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 42 |
+
}
|
| 43 |
+
}
|
src/app/api/contributor/claim-activity/[issueId]/route.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Claim Activity Route
|
| 3 |
+
*
|
| 4 |
+
* POST /api/contributor/claim-activity/[issueId]
|
| 5 |
+
* Update activity timestamp for a claimed issue
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
|
| 11 |
+
export async function POST(
|
| 12 |
+
request: NextRequest,
|
| 13 |
+
{ params }: { params: Promise<{ issueId: string }> }
|
| 14 |
+
) {
|
| 15 |
+
try {
|
| 16 |
+
const user = await getCurrentUser(request);
|
| 17 |
+
if (!user) {
|
| 18 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const { issueId } = await params;
|
| 22 |
+
|
| 23 |
+
// TODO: Implement with claimed_issues table
|
| 24 |
+
return NextResponse.json({
|
| 25 |
+
message: "Activity updated successfully (stub)",
|
| 26 |
+
issueId,
|
| 27 |
+
});
|
| 28 |
+
} catch (error) {
|
| 29 |
+
console.error("Claim activity error:", error);
|
| 30 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 31 |
+
}
|
| 32 |
+
}
|
src/app/api/contributor/claim-issue/[issueId]/route.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Unclaim Issue Route
|
| 3 |
+
*
|
| 4 |
+
* DELETE /api/contributor/claim-issue/[issueId]
|
| 5 |
+
* Unclaim a previously claimed issue
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
|
| 11 |
+
export async function DELETE(
|
| 12 |
+
request: NextRequest,
|
| 13 |
+
{ params }: { params: Promise<{ issueId: string }> }
|
| 14 |
+
) {
|
| 15 |
+
try {
|
| 16 |
+
const user = await getCurrentUser(request);
|
| 17 |
+
if (!user) {
|
| 18 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const { issueId } = await params;
|
| 22 |
+
|
| 23 |
+
// TODO: Implement full unclaiming logic with claimed_issues table
|
| 24 |
+
return NextResponse.json({
|
| 25 |
+
message: "Issue unclaimed successfully (stub)",
|
| 26 |
+
issueId,
|
| 27 |
+
});
|
| 28 |
+
} catch (error) {
|
| 29 |
+
console.error("Unclaim issue error:", error);
|
| 30 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 31 |
+
}
|
| 32 |
+
}
|
src/app/api/contributor/claim-issue/route.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Issue Claiming Routes
|
| 3 |
+
*
|
| 4 |
+
* POST /api/contributor/claim-issue
|
| 5 |
+
* Claim an issue to work on
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
|
| 11 |
+
// Note: Full implementation requires adding claimed_issues table to schema
|
| 12 |
+
// This is a stub that prevents 404 errors
|
| 13 |
+
|
| 14 |
+
export async function POST(request: NextRequest) {
|
| 15 |
+
try {
|
| 16 |
+
const user = await getCurrentUser(request);
|
| 17 |
+
if (!user) {
|
| 18 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const body = await request.json();
|
| 22 |
+
const { issueId } = body;
|
| 23 |
+
|
| 24 |
+
if (!issueId) {
|
| 25 |
+
return NextResponse.json({ error: "issueId is required" }, { status: 400 });
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// TODO: Implement full claiming logic with claimed_issues table
|
| 29 |
+
return NextResponse.json({
|
| 30 |
+
message: "Issue claim registered (stub)",
|
| 31 |
+
issueId,
|
| 32 |
+
claimedAt: new Date().toISOString(),
|
| 33 |
+
});
|
| 34 |
+
} catch (error) {
|
| 35 |
+
console.error("Claim issue error:", error);
|
| 36 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 37 |
+
}
|
| 38 |
+
}
|
src/app/api/contributor/dashboard-summary/route.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Contributor Dashboard Summary Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/contributor/dashboard-summary
|
| 5 |
+
* Get dashboard statistics for the contributor
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { getIssuesWithTriage } from "@/lib/db/queries/issues";
|
| 11 |
+
import { getContributorRepositories } from "@/lib/db/queries/repositories";
|
| 12 |
+
|
| 13 |
+
export async function GET(request: NextRequest) {
|
| 14 |
+
try {
|
| 15 |
+
const user = await getCurrentUser(request);
|
| 16 |
+
if (!user) {
|
| 17 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Get all contributor's issues (no pagination for stats)
|
| 21 |
+
const [repos, issuesData] = await Promise.all([
|
| 22 |
+
getContributorRepositories(user.id, user.username),
|
| 23 |
+
getIssuesWithTriage({ authorName: user.username }, 1, 1000), // Get all for accurate stats
|
| 24 |
+
]);
|
| 25 |
+
|
| 26 |
+
const allIssues = issuesData.issues;
|
| 27 |
+
const myIssues = allIssues.filter(i => !i.isPR);
|
| 28 |
+
const myPRs = allIssues.filter(i => i.isPR);
|
| 29 |
+
|
| 30 |
+
return NextResponse.json({
|
| 31 |
+
totalContributions: allIssues.length,
|
| 32 |
+
totalPRs: myPRs.length,
|
| 33 |
+
openPRs: myPRs.filter(i => i.state === "open").length,
|
| 34 |
+
mergedPRs: myPRs.filter(i => i.state === "closed").length,
|
| 35 |
+
totalIssues: myIssues.length,
|
| 36 |
+
openIssues: myIssues.filter(i => i.state === "open").length,
|
| 37 |
+
closedIssues: myIssues.filter(i => i.state === "closed").length,
|
| 38 |
+
repositoriesContributed: repos.length,
|
| 39 |
+
});
|
| 40 |
+
} catch (error) {
|
| 41 |
+
console.error("Contributor dashboard-summary error:", error);
|
| 42 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 43 |
+
}
|
| 44 |
+
}
|
src/app/api/contributor/my-claimed-issues/route.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* My Claimed Issues Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/contributor/my-claimed-issues
|
| 5 |
+
* Get all issues claimed by the current user
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
|
| 11 |
+
export async function GET(request: NextRequest) {
|
| 12 |
+
try {
|
| 13 |
+
const user = await getCurrentUser(request);
|
| 14 |
+
if (!user) {
|
| 15 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// TODO: Implement with claimed_issues table
|
| 19 |
+
return NextResponse.json({
|
| 20 |
+
claims: [],
|
| 21 |
+
count: 0,
|
| 22 |
+
});
|
| 23 |
+
} catch (error) {
|
| 24 |
+
console.error("My claimed issues error:", error);
|
| 25 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 26 |
+
}
|
| 27 |
+
}
|
src/app/api/contributor/my-issues/route.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Contributor My Issues Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/contributor/my-issues
|
| 5 |
+
* Get paginated list of contributor's issues and PRs
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { getIssuesWithTriage } from "@/lib/db/queries/issues";
|
| 11 |
+
|
| 12 |
+
export async function GET(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const { searchParams } = new URL(request.url);
|
| 20 |
+
const page = parseInt(searchParams.get("page") || "1");
|
| 21 |
+
const limit = parseInt(searchParams.get("limit") || "10");
|
| 22 |
+
|
| 23 |
+
const issuesData = await getIssuesWithTriage({ authorName: user.username }, page, limit);
|
| 24 |
+
|
| 25 |
+
return NextResponse.json({
|
| 26 |
+
items: issuesData.issues,
|
| 27 |
+
total: issuesData.total,
|
| 28 |
+
page: issuesData.page,
|
| 29 |
+
pages: issuesData.totalPages,
|
| 30 |
+
limit: issuesData.limit,
|
| 31 |
+
});
|
| 32 |
+
} catch (error) {
|
| 33 |
+
console.error("Contributor my-issues error:", error);
|
| 34 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 35 |
+
}
|
| 36 |
+
}
|
src/app/api/contributor/route.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Contributor Dashboard Route
|
| 3 |
+
*
|
| 4 |
+
* Get dashboard stats and issues for contributors.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 8 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 9 |
+
import { getIssuesWithTriage } from "@/lib/db/queries/issues";
|
| 10 |
+
import { getContributorRepositories } from "@/lib/db/queries/repositories";
|
| 11 |
+
|
| 12 |
+
export async function GET(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const { searchParams } = new URL(request.url);
|
| 20 |
+
const page = parseInt(searchParams.get("page") || "1");
|
| 21 |
+
const limit = parseInt(searchParams.get("limit") || "10");
|
| 22 |
+
|
| 23 |
+
// Get contributor's issues
|
| 24 |
+
const [repos, issuesData] = await Promise.all([
|
| 25 |
+
getContributorRepositories(user.id, user.username),
|
| 26 |
+
getIssuesWithTriage({ authorName: user.username }, page, limit),
|
| 27 |
+
]);
|
| 28 |
+
|
| 29 |
+
// Calculate stats
|
| 30 |
+
const myIssues = issuesData.issues.filter(i => !i.isPR);
|
| 31 |
+
const myPRs = issuesData.issues.filter(i => i.isPR);
|
| 32 |
+
|
| 33 |
+
return NextResponse.json({
|
| 34 |
+
stats: {
|
| 35 |
+
totalIssues: myIssues.length,
|
| 36 |
+
totalPRs: myPRs.length,
|
| 37 |
+
openIssues: myIssues.filter(i => i.state === "open").length,
|
| 38 |
+
openPRs: myPRs.filter(i => i.state === "open").length,
|
| 39 |
+
repositoriesContributed: repos.length,
|
| 40 |
+
},
|
| 41 |
+
repositories: repos,
|
| 42 |
+
issues: issuesData.issues,
|
| 43 |
+
pagination: {
|
| 44 |
+
page: issuesData.page,
|
| 45 |
+
limit: issuesData.limit,
|
| 46 |
+
total: issuesData.total,
|
| 47 |
+
totalPages: issuesData.totalPages,
|
| 48 |
+
}
|
| 49 |
+
});
|
| 50 |
+
} catch (error) {
|
| 51 |
+
console.error("Contributor dashboard error:", error);
|
| 52 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 53 |
+
}
|
| 54 |
+
}
|
src/app/api/issues/[id]/messages/route.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { db } from "@/db";
|
| 3 |
+
import { chatMessages, issues } from "@/db/schema";
|
| 4 |
+
import { requireAuth } from "@/lib/auth";
|
| 5 |
+
import { generateId, now } from "@/lib/utils";
|
| 6 |
+
import { eq, desc, gt, and } from "drizzle-orm";
|
| 7 |
+
|
| 8 |
+
type RouteContext = {
|
| 9 |
+
params: Promise<{ id: string }>;
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* GET /api/issues/[id]/messages
|
| 14 |
+
* Get chat messages for an issue.
|
| 15 |
+
*/
|
| 16 |
+
export async function GET(
|
| 17 |
+
request: NextRequest,
|
| 18 |
+
context: RouteContext
|
| 19 |
+
) {
|
| 20 |
+
const { id: issueId } = await context.params;
|
| 21 |
+
const { searchParams } = new URL(request.url);
|
| 22 |
+
const afterId = searchParams.get("afterId");
|
| 23 |
+
const limit = parseInt(searchParams.get("limit") || "50", 10);
|
| 24 |
+
|
| 25 |
+
try {
|
| 26 |
+
// Verify issue exists
|
| 27 |
+
const issueRecords = await db
|
| 28 |
+
.select()
|
| 29 |
+
.from(issues)
|
| 30 |
+
.where(eq(issues.id, issueId))
|
| 31 |
+
.limit(1);
|
| 32 |
+
|
| 33 |
+
if (issueRecords.length === 0) {
|
| 34 |
+
return NextResponse.json(
|
| 35 |
+
{ error: "Issue not found" },
|
| 36 |
+
{ status: 404 }
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Build query for messages
|
| 41 |
+
// Note: We use sessionId to associate messages with issues
|
| 42 |
+
// In a proper implementation, you'd have a session per issue
|
| 43 |
+
let messagesQuery = db
|
| 44 |
+
.select()
|
| 45 |
+
.from(chatMessages)
|
| 46 |
+
.where(eq(chatMessages.sessionId, issueId))
|
| 47 |
+
.orderBy(desc(chatMessages.timestamp))
|
| 48 |
+
.limit(limit);
|
| 49 |
+
|
| 50 |
+
const messages = await messagesQuery;
|
| 51 |
+
|
| 52 |
+
// Reverse to show oldest first
|
| 53 |
+
messages.reverse();
|
| 54 |
+
|
| 55 |
+
return NextResponse.json({
|
| 56 |
+
messages,
|
| 57 |
+
count: messages.length,
|
| 58 |
+
issueId,
|
| 59 |
+
});
|
| 60 |
+
} catch (error) {
|
| 61 |
+
console.error("GET /api/issues/[id]/messages error:", error);
|
| 62 |
+
return NextResponse.json(
|
| 63 |
+
{ error: "Failed to fetch messages" },
|
| 64 |
+
{ status: 500 }
|
| 65 |
+
);
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* POST /api/issues/[id]/messages
|
| 71 |
+
* Send a chat message for an issue.
|
| 72 |
+
*
|
| 73 |
+
* Request body: { content: string, messageType?: string }
|
| 74 |
+
*/
|
| 75 |
+
export async function POST(
|
| 76 |
+
request: NextRequest,
|
| 77 |
+
context: RouteContext
|
| 78 |
+
) {
|
| 79 |
+
const { user, error } = await requireAuth(request);
|
| 80 |
+
if (error) return error;
|
| 81 |
+
|
| 82 |
+
const { id: issueId } = await context.params;
|
| 83 |
+
|
| 84 |
+
try {
|
| 85 |
+
const body = await request.json();
|
| 86 |
+
const { content, messageType = "text" } = body;
|
| 87 |
+
|
| 88 |
+
if (!content || typeof content !== "string") {
|
| 89 |
+
return NextResponse.json(
|
| 90 |
+
{ error: "content is required" },
|
| 91 |
+
{ status: 400 }
|
| 92 |
+
);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Verify issue exists
|
| 96 |
+
const issueRecords = await db
|
| 97 |
+
.select()
|
| 98 |
+
.from(issues)
|
| 99 |
+
.where(eq(issues.id, issueId))
|
| 100 |
+
.limit(1);
|
| 101 |
+
|
| 102 |
+
if (issueRecords.length === 0) {
|
| 103 |
+
return NextResponse.json(
|
| 104 |
+
{ error: "Issue not found" },
|
| 105 |
+
{ status: 404 }
|
| 106 |
+
);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Create the message
|
| 110 |
+
const message = {
|
| 111 |
+
id: generateId(),
|
| 112 |
+
sessionId: issueId, // Using issueId as sessionId for issue-based chats
|
| 113 |
+
senderId: user!.id,
|
| 114 |
+
senderUsername: user!.username,
|
| 115 |
+
isMentor: false,
|
| 116 |
+
content,
|
| 117 |
+
messageType,
|
| 118 |
+
language: null,
|
| 119 |
+
isAiGenerated: false,
|
| 120 |
+
containsResource: false,
|
| 121 |
+
extractedResourceId: null,
|
| 122 |
+
timestamp: now(),
|
| 123 |
+
editedAt: null,
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
await db.insert(chatMessages).values(message);
|
| 127 |
+
|
| 128 |
+
return NextResponse.json({
|
| 129 |
+
message: "Message sent",
|
| 130 |
+
data: message,
|
| 131 |
+
}, { status: 201 });
|
| 132 |
+
} catch (error) {
|
| 133 |
+
console.error("POST /api/issues/[id]/messages error:", error);
|
| 134 |
+
return NextResponse.json(
|
| 135 |
+
{ error: "Failed to send message" },
|
| 136 |
+
{ status: 500 }
|
| 137 |
+
);
|
| 138 |
+
}
|
| 139 |
+
}
|
src/app/api/issues/route.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { db } from "@/db";
|
| 3 |
+
import { issues, triageData, repositories } from "@/db/schema";
|
| 4 |
+
import { generateId, now } from "@/lib/utils";
|
| 5 |
+
import { eq, and, desc } from "drizzle-orm";
|
| 6 |
+
|
| 7 |
+
// =============================================================================
|
| 8 |
+
// GET /api/issues - Get issues for a repository
|
| 9 |
+
// =============================================================================
|
| 10 |
+
//
|
| 11 |
+
// Python equivalent (from routes/repository.py):
|
| 12 |
+
// repos = await db.issues.find({"repoId": repo_id}, {"_id": 0}).to_list(1000)
|
| 13 |
+
//
|
| 14 |
+
export async function GET(request: NextRequest) {
|
| 15 |
+
try {
|
| 16 |
+
const { searchParams } = new URL(request.url);
|
| 17 |
+
const repoId = searchParams.get("repoId");
|
| 18 |
+
const state = searchParams.get("state"); // open, closed, all
|
| 19 |
+
const isPR = searchParams.get("isPR"); // true, false
|
| 20 |
+
|
| 21 |
+
// Build query conditions
|
| 22 |
+
const conditions = [];
|
| 23 |
+
if (repoId) {
|
| 24 |
+
conditions.push(eq(issues.repoId, repoId));
|
| 25 |
+
}
|
| 26 |
+
if (state && state !== "all") {
|
| 27 |
+
conditions.push(eq(issues.state, state));
|
| 28 |
+
}
|
| 29 |
+
if (isPR !== null && isPR !== undefined) {
|
| 30 |
+
conditions.push(eq(issues.isPR, isPR === "true"));
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Execute query with Drizzle
|
| 34 |
+
const result = await db
|
| 35 |
+
.select()
|
| 36 |
+
.from(issues)
|
| 37 |
+
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
| 38 |
+
.orderBy(desc(issues.createdAt))
|
| 39 |
+
.limit(100);
|
| 40 |
+
|
| 41 |
+
return NextResponse.json(result, { status: 200 });
|
| 42 |
+
} catch (error) {
|
| 43 |
+
console.error("GET /api/issues error:", error);
|
| 44 |
+
return NextResponse.json(
|
| 45 |
+
{ error: "Failed to fetch issues" },
|
| 46 |
+
{ status: 500 }
|
| 47 |
+
);
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// =============================================================================
|
| 52 |
+
// POST /api/issues - Create a new issue
|
| 53 |
+
// =============================================================================
|
| 54 |
+
//
|
| 55 |
+
// Python equivalent (from routes/repository.py):
|
| 56 |
+
// issue = Issue(
|
| 57 |
+
// githubIssueId=gh_issue['id'],
|
| 58 |
+
// number=gh_issue['number'],
|
| 59 |
+
// title=gh_issue['title'],
|
| 60 |
+
// body=gh_issue.get('body') or '',
|
| 61 |
+
// authorName=gh_issue['user']['login'],
|
| 62 |
+
// repoId=repository.id,
|
| 63 |
+
// repoName=repository.name,
|
| 64 |
+
// ...
|
| 65 |
+
// )
|
| 66 |
+
// issue_dict = issue.model_dump()
|
| 67 |
+
// issue_dict['createdAt'] = issue_dict['createdAt'].isoformat()
|
| 68 |
+
// await db.issues.insert_one(issue_dict)
|
| 69 |
+
//
|
| 70 |
+
export async function POST(request: NextRequest) {
|
| 71 |
+
try {
|
| 72 |
+
const body = await request.json();
|
| 73 |
+
|
| 74 |
+
// Validate required fields
|
| 75 |
+
const { githubIssueId, number, title, authorName, repoId, repoName } = body;
|
| 76 |
+
if (!githubIssueId || !number || !title || !authorName || !repoId || !repoName) {
|
| 77 |
+
return NextResponse.json(
|
| 78 |
+
{ error: "Missing required fields" },
|
| 79 |
+
{ status: 400 }
|
| 80 |
+
);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Check if repository exists
|
| 84 |
+
const repo = await db
|
| 85 |
+
.select()
|
| 86 |
+
.from(repositories)
|
| 87 |
+
.where(eq(repositories.id, repoId))
|
| 88 |
+
.limit(1);
|
| 89 |
+
|
| 90 |
+
if (repo.length === 0) {
|
| 91 |
+
return NextResponse.json(
|
| 92 |
+
{ error: "Repository not found" },
|
| 93 |
+
{ status: 404 }
|
| 94 |
+
);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// Check if issue already exists
|
| 98 |
+
const existingIssue = await db
|
| 99 |
+
.select()
|
| 100 |
+
.from(issues)
|
| 101 |
+
.where(eq(issues.githubIssueId, githubIssueId))
|
| 102 |
+
.limit(1);
|
| 103 |
+
|
| 104 |
+
if (existingIssue.length > 0) {
|
| 105 |
+
return NextResponse.json(
|
| 106 |
+
{ error: "Issue already exists", issue: existingIssue[0] },
|
| 107 |
+
{ status: 409 }
|
| 108 |
+
);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Create new issue
|
| 112 |
+
const newIssue = {
|
| 113 |
+
id: generateId(),
|
| 114 |
+
githubIssueId,
|
| 115 |
+
number,
|
| 116 |
+
title,
|
| 117 |
+
body: body.body || "",
|
| 118 |
+
authorName,
|
| 119 |
+
repoId,
|
| 120 |
+
repoName,
|
| 121 |
+
owner: body.owner || "",
|
| 122 |
+
repo: body.repo || "",
|
| 123 |
+
htmlUrl: body.htmlUrl || "",
|
| 124 |
+
state: body.state || "open",
|
| 125 |
+
isPR: body.isPR || false,
|
| 126 |
+
createdAt: now(),
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
await db.insert(issues).values(newIssue);
|
| 130 |
+
|
| 131 |
+
return NextResponse.json(
|
| 132 |
+
{ message: "Issue created", issue: newIssue },
|
| 133 |
+
{ status: 201 }
|
| 134 |
+
);
|
| 135 |
+
} catch (error) {
|
| 136 |
+
console.error("POST /api/issues error:", error);
|
| 137 |
+
return NextResponse.json(
|
| 138 |
+
{ error: "Failed to create issue" },
|
| 139 |
+
{ status: 500 }
|
| 140 |
+
);
|
| 141 |
+
}
|
| 142 |
+
}
|
src/app/api/maintainer/dashboard-summary/route.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Maintainer Dashboard Summary Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/maintainer/dashboard-summary
|
| 5 |
+
* Get dashboard statistics for maintainers
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { getDashboardStats } from "@/lib/db/queries/issues";
|
| 11 |
+
import { getMaintainerRepositories } from "@/lib/db/queries/repositories";
|
| 12 |
+
|
| 13 |
+
export async function GET(request: NextRequest) {
|
| 14 |
+
try {
|
| 15 |
+
const user = await getCurrentUser(request);
|
| 16 |
+
if (!user) {
|
| 17 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
if (user.role !== "MAINTAINER" && user.role !== "maintainer") {
|
| 21 |
+
return NextResponse.json({ error: "Maintainer access required" }, { status: 403 });
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// Get dashboard stats
|
| 25 |
+
const [stats, repos] = await Promise.all([
|
| 26 |
+
getDashboardStats(user.id),
|
| 27 |
+
getMaintainerRepositories(user.id),
|
| 28 |
+
]);
|
| 29 |
+
|
| 30 |
+
return NextResponse.json({
|
| 31 |
+
...stats,
|
| 32 |
+
repositoriesCount: repos.length,
|
| 33 |
+
repositories: repos,
|
| 34 |
+
});
|
| 35 |
+
} catch (error) {
|
| 36 |
+
console.error("Maintainer dashboard-summary error:", error);
|
| 37 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 38 |
+
}
|
| 39 |
+
}
|
src/app/api/maintainer/issues/route.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Maintainer Issues Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/maintainer/issues
|
| 5 |
+
* Fetch issues for a maintainer with filtering and pagination.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { getIssues, getIssuesWithTriage, IssueFilters } from "@/lib/db/queries/issues";
|
| 11 |
+
|
| 12 |
+
export async function GET(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const { searchParams } = new URL(request.url);
|
| 20 |
+
const page = parseInt(searchParams.get("page") || "1");
|
| 21 |
+
const limit = parseInt(searchParams.get("limit") || "10");
|
| 22 |
+
const state = searchParams.get("state") || undefined;
|
| 23 |
+
const repoId = searchParams.get("repoId") || undefined;
|
| 24 |
+
const search = searchParams.get("search") || undefined;
|
| 25 |
+
const withTriage = searchParams.get("withTriage") === "true";
|
| 26 |
+
|
| 27 |
+
const filters: IssueFilters = {
|
| 28 |
+
userId: user.id, // Filter by user's repos
|
| 29 |
+
state,
|
| 30 |
+
repoId,
|
| 31 |
+
search,
|
| 32 |
+
isPR: false, // Only fetch issues, not PRs
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
const result = withTriage
|
| 36 |
+
? await getIssuesWithTriage(filters, page, limit)
|
| 37 |
+
: await getIssues(filters, page, limit);
|
| 38 |
+
|
| 39 |
+
return NextResponse.json(result);
|
| 40 |
+
} catch (error) {
|
| 41 |
+
console.error("GET /api/maintainer/issues error:", error);
|
| 42 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 43 |
+
}
|
| 44 |
+
}
|
src/app/api/maintainer/route.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Maintainer Dashboard Route
|
| 3 |
+
*
|
| 4 |
+
* Get dashboard stats, issues, and templates for maintainers.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 8 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 9 |
+
import { getDashboardStats, getIssuesWithTriage } from "@/lib/db/queries/issues";
|
| 10 |
+
import { getMaintainerRepositories, getRepositoryStats } from "@/lib/db/queries/repositories";
|
| 11 |
+
|
| 12 |
+
export async function GET(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
if (user.role !== "MAINTAINER" && user.role !== "maintainer") {
|
| 20 |
+
return NextResponse.json({ error: "Maintainer access required" }, { status: 403 });
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const { searchParams } = new URL(request.url);
|
| 24 |
+
const page = parseInt(searchParams.get("page") || "1");
|
| 25 |
+
const limit = parseInt(searchParams.get("limit") || "10");
|
| 26 |
+
|
| 27 |
+
// Get dashboard data
|
| 28 |
+
const [stats, repos, issuesData] = await Promise.all([
|
| 29 |
+
getDashboardStats(user.id),
|
| 30 |
+
getMaintainerRepositories(user.id),
|
| 31 |
+
getIssuesWithTriage({ userId: user.id, state: "open" }, page, limit),
|
| 32 |
+
]);
|
| 33 |
+
|
| 34 |
+
return NextResponse.json({
|
| 35 |
+
stats,
|
| 36 |
+
repositories: repos,
|
| 37 |
+
issues: issuesData.issues,
|
| 38 |
+
pagination: {
|
| 39 |
+
page: issuesData.page,
|
| 40 |
+
limit: issuesData.limit,
|
| 41 |
+
total: issuesData.total,
|
| 42 |
+
totalPages: issuesData.totalPages,
|
| 43 |
+
}
|
| 44 |
+
});
|
| 45 |
+
} catch (error) {
|
| 46 |
+
console.error("Maintainer dashboard error:", error);
|
| 47 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 48 |
+
}
|
| 49 |
+
}
|
src/app/api/maintainer/templates/route.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Maintainer Templates Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/maintainer/templates
|
| 5 |
+
* Fetch templates for a maintainer.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { getTemplatesByOwnerId } from "@/lib/db/queries/templates";
|
| 11 |
+
|
| 12 |
+
export async function GET(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const templates = await getTemplatesByOwnerId(user.id);
|
| 20 |
+
return NextResponse.json(templates);
|
| 21 |
+
} catch (error) {
|
| 22 |
+
console.error("GET /api/maintainer/templates error:", error);
|
| 23 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 24 |
+
}
|
| 25 |
+
}
|
src/app/api/messages/route.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Messages Route
|
| 3 |
+
*
|
| 4 |
+
* Get conversations and send messages.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 8 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 9 |
+
import { getConversations, getChatHistory, sendMessage, markMessagesAsRead, getUnreadCount } from "@/lib/db/queries/messages";
|
| 10 |
+
|
| 11 |
+
export async function GET(request: NextRequest) {
|
| 12 |
+
try {
|
| 13 |
+
const user = await getCurrentUser(request);
|
| 14 |
+
if (!user) {
|
| 15 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const { searchParams } = new URL(request.url);
|
| 19 |
+
const otherUserId = searchParams.get("with");
|
| 20 |
+
|
| 21 |
+
if (otherUserId) {
|
| 22 |
+
// Get chat history with specific user
|
| 23 |
+
const history = await getChatHistory(user.id, otherUserId);
|
| 24 |
+
await markMessagesAsRead(user.id, otherUserId);
|
| 25 |
+
return NextResponse.json({ messages: history });
|
| 26 |
+
} else {
|
| 27 |
+
// Get all conversations
|
| 28 |
+
const conversations = await getConversations(user.id);
|
| 29 |
+
const unreadCount = await getUnreadCount(user.id);
|
| 30 |
+
return NextResponse.json({ conversations, unreadCount });
|
| 31 |
+
}
|
| 32 |
+
} catch (error) {
|
| 33 |
+
console.error("Messages fetch error:", error);
|
| 34 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export async function POST(request: NextRequest) {
|
| 39 |
+
try {
|
| 40 |
+
const user = await getCurrentUser(request);
|
| 41 |
+
if (!user) {
|
| 42 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
const body = await request.json();
|
| 46 |
+
const { receiverId, content } = body;
|
| 47 |
+
|
| 48 |
+
if (!receiverId || !content) {
|
| 49 |
+
return NextResponse.json({ error: "receiverId and content are required" }, { status: 400 });
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
const message = await sendMessage({
|
| 53 |
+
senderId: user.id,
|
| 54 |
+
receiverId,
|
| 55 |
+
content,
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
return NextResponse.json(message);
|
| 59 |
+
} catch (error) {
|
| 60 |
+
console.error("Message send error:", error);
|
| 61 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 62 |
+
}
|
| 63 |
+
}
|
src/app/api/messaging/conversations/route.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Messaging Conversations Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/messaging/conversations
|
| 5 |
+
* Get list of all conversations for the current user
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { getConversations } from "@/lib/db/queries/messages";
|
| 11 |
+
|
| 12 |
+
export async function GET(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const conversations = await getConversations(user.id);
|
| 20 |
+
|
| 21 |
+
// Format to match frontend expectations
|
| 22 |
+
return NextResponse.json({
|
| 23 |
+
conversations: conversations.map(c => ({
|
| 24 |
+
user_id: c.partnerId,
|
| 25 |
+
username: c.partnerUsername,
|
| 26 |
+
avatar_url: c.partnerAvatar,
|
| 27 |
+
last_message: c.lastMessage?.content?.substring(0, 50) || "",
|
| 28 |
+
last_timestamp: c.lastMessage?.timestamp || null,
|
| 29 |
+
unread_count: c.unreadCount
|
| 30 |
+
}))
|
| 31 |
+
});
|
| 32 |
+
} catch (error) {
|
| 33 |
+
console.error("Conversations error:", error);
|
| 34 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 35 |
+
}
|
| 36 |
+
}
|
src/app/api/messaging/history/[userId]/route.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Messaging History Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/messaging/history/[userId]
|
| 5 |
+
* Get chat history with a specific user
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { getChatHistory, markMessagesAsRead } from "@/lib/db/queries/messages";
|
| 11 |
+
|
| 12 |
+
export async function GET(
|
| 13 |
+
request: NextRequest,
|
| 14 |
+
{ params }: { params: Promise<{ userId: string }> }
|
| 15 |
+
) {
|
| 16 |
+
try {
|
| 17 |
+
const user = await getCurrentUser(request);
|
| 18 |
+
if (!user) {
|
| 19 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const { userId: otherUserId } = await params;
|
| 23 |
+
|
| 24 |
+
// Get chat history
|
| 25 |
+
const history = await getChatHistory(user.id, otherUserId);
|
| 26 |
+
|
| 27 |
+
// Mark messages as read
|
| 28 |
+
await markMessagesAsRead(user.id, otherUserId);
|
| 29 |
+
|
| 30 |
+
return NextResponse.json(history);
|
| 31 |
+
} catch (error) {
|
| 32 |
+
console.error("Chat history error:", error);
|
| 33 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 34 |
+
}
|
| 35 |
+
}
|
src/app/api/messaging/mark-read/[userId]/route.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Messaging Mark Read Route
|
| 3 |
+
*
|
| 4 |
+
* POST /api/messaging/mark-read/[userId]
|
| 5 |
+
* Mark all messages from a user as read
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { markMessagesAsRead } from "@/lib/db/queries/messages";
|
| 11 |
+
|
| 12 |
+
export async function POST(
|
| 13 |
+
request: NextRequest,
|
| 14 |
+
{ params }: { params: Promise<{ userId: string }> }
|
| 15 |
+
) {
|
| 16 |
+
try {
|
| 17 |
+
const user = await getCurrentUser(request);
|
| 18 |
+
if (!user) {
|
| 19 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const { userId: otherUserId } = await params;
|
| 23 |
+
|
| 24 |
+
await markMessagesAsRead(user.id, otherUserId);
|
| 25 |
+
|
| 26 |
+
return NextResponse.json({ success: true, message: "Messages marked as read" });
|
| 27 |
+
} catch (error) {
|
| 28 |
+
console.error("Mark read error:", error);
|
| 29 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 30 |
+
}
|
| 31 |
+
}
|
src/app/api/messaging/poll/[userId]/route.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Messaging Poll Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/messaging/poll/[userId]
|
| 5 |
+
* Poll for new messages from a specific user
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { pollNewMessages } from "@/lib/db/queries/messages";
|
| 11 |
+
|
| 12 |
+
export async function GET(
|
| 13 |
+
request: NextRequest,
|
| 14 |
+
{ params }: { params: Promise<{ userId: string }> }
|
| 15 |
+
) {
|
| 16 |
+
try {
|
| 17 |
+
const user = await getCurrentUser(request);
|
| 18 |
+
if (!user) {
|
| 19 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const { userId: otherUserId } = await params;
|
| 23 |
+
const { searchParams } = new URL(request.url);
|
| 24 |
+
const lastMessageId = searchParams.get("last_message_id") || undefined;
|
| 25 |
+
|
| 26 |
+
const newMessages = await pollNewMessages(user.id, otherUserId, lastMessageId);
|
| 27 |
+
|
| 28 |
+
return NextResponse.json(newMessages);
|
| 29 |
+
} catch (error) {
|
| 30 |
+
console.error("Poll messages error:", error);
|
| 31 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 32 |
+
}
|
| 33 |
+
}
|
src/app/api/messaging/route.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Messaging Routes
|
| 3 |
+
*
|
| 4 |
+
* Full messaging API for the frontend
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 8 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 9 |
+
import {
|
| 10 |
+
getConversations,
|
| 11 |
+
getChatHistory,
|
| 12 |
+
sendMessage,
|
| 13 |
+
markMessagesAsRead,
|
| 14 |
+
getUnreadCount,
|
| 15 |
+
pollNewMessages
|
| 16 |
+
} from "@/lib/db/queries/messages";
|
| 17 |
+
|
| 18 |
+
// GET /api/messaging - Get conversations list
|
| 19 |
+
export async function GET(request: NextRequest) {
|
| 20 |
+
try {
|
| 21 |
+
const user = await getCurrentUser(request);
|
| 22 |
+
if (!user) {
|
| 23 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const conversations = await getConversations(user.id);
|
| 27 |
+
const unreadCount = await getUnreadCount(user.id);
|
| 28 |
+
|
| 29 |
+
return NextResponse.json({ conversations, unreadCount });
|
| 30 |
+
} catch (error) {
|
| 31 |
+
console.error("Messaging error:", error);
|
| 32 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 33 |
+
}
|
| 34 |
+
}
|
src/app/api/messaging/send/route.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Messaging Send Route
|
| 3 |
+
*
|
| 4 |
+
* POST /api/messaging/send
|
| 5 |
+
* Send a message to another user
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { sendMessage } from "@/lib/db/queries/messages";
|
| 11 |
+
|
| 12 |
+
export async function POST(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const body = await request.json();
|
| 20 |
+
const { receiver_id, content } = body;
|
| 21 |
+
|
| 22 |
+
if (!receiver_id || !content) {
|
| 23 |
+
return NextResponse.json(
|
| 24 |
+
{ error: "receiver_id and content are required" },
|
| 25 |
+
{ status: 400 }
|
| 26 |
+
);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const message = await sendMessage({
|
| 30 |
+
senderId: user.id,
|
| 31 |
+
receiverId: receiver_id,
|
| 32 |
+
content,
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
return NextResponse.json(message);
|
| 36 |
+
} catch (error) {
|
| 37 |
+
console.error("Send message error:", error);
|
| 38 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 39 |
+
}
|
| 40 |
+
}
|
src/app/api/messaging/unread-count/route.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Messaging Unread Count Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/messaging/unread-count
|
| 5 |
+
* Get the count of unread messages for the authenticated user
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { getUnreadCount } from "@/lib/db/queries/messages";
|
| 11 |
+
|
| 12 |
+
export async function GET(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const count = await getUnreadCount(user.id);
|
| 20 |
+
|
| 21 |
+
return NextResponse.json({ count });
|
| 22 |
+
} catch (error) {
|
| 23 |
+
console.error("Unread count error:", error);
|
| 24 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 25 |
+
}
|
| 26 |
+
}
|
src/app/api/profile/[id]/connected-repos/route.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { db } from "@/db";
|
| 3 |
+
import { profileConnectedRepos } from "@/db/schema";
|
| 4 |
+
import { eq } from "drizzle-orm";
|
| 5 |
+
|
| 6 |
+
export async function GET(
|
| 7 |
+
request: NextRequest,
|
| 8 |
+
context: { params: Promise<{ id: string }> }
|
| 9 |
+
) {
|
| 10 |
+
try {
|
| 11 |
+
const { id } = await context.params;
|
| 12 |
+
// 'id' here corresponds to the profile's userId (since userId is PK for profiles)
|
| 13 |
+
const repos = await db
|
| 14 |
+
.select()
|
| 15 |
+
.from(profileConnectedRepos)
|
| 16 |
+
.where(eq(profileConnectedRepos.profileId, id));
|
| 17 |
+
|
| 18 |
+
return NextResponse.json(repos);
|
| 19 |
+
} catch (error) {
|
| 20 |
+
console.error("GET /api/profile/:id/connected-repos error:", error);
|
| 21 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 22 |
+
}
|
| 23 |
+
}
|
src/app/api/profile/[username]/featured-badges/route.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { getUserBadges } from "@/lib/db/queries/gamification";
|
| 3 |
+
|
| 4 |
+
export async function GET(
|
| 5 |
+
request: NextRequest,
|
| 6 |
+
context: { params: Promise<{ username: string }> }
|
| 7 |
+
) {
|
| 8 |
+
try {
|
| 9 |
+
const { username } = await context.params;
|
| 10 |
+
const badges = await getUserBadges(username);
|
| 11 |
+
// For now, just return top 5 badges as "featured"
|
| 12 |
+
// In the future, we could add a "featured" flag to the trophy table
|
| 13 |
+
const featured = badges.slice(0, 5);
|
| 14 |
+
|
| 15 |
+
return NextResponse.json(featured);
|
| 16 |
+
} catch (error) {
|
| 17 |
+
console.error("GET /api/profile/:username/featured-badges error:", error);
|
| 18 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 19 |
+
}
|
| 20 |
+
}
|
src/app/api/profile/[username]/repos/route.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { db } from "@/db";
|
| 3 |
+
import { repositories, users } from "@/db/schema";
|
| 4 |
+
import { eq, desc } from "drizzle-orm";
|
| 5 |
+
|
| 6 |
+
export async function GET(
|
| 7 |
+
request: NextRequest,
|
| 8 |
+
context: { params: Promise<{ username: string }> }
|
| 9 |
+
) {
|
| 10 |
+
try {
|
| 11 |
+
const { username } = await context.params;
|
| 12 |
+
|
| 13 |
+
// Find user first to get ID
|
| 14 |
+
const user = await db.select().from(users).where(eq(users.username, username)).limit(1);
|
| 15 |
+
|
| 16 |
+
if (!user[0]) {
|
| 17 |
+
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const repos = await db
|
| 21 |
+
.select()
|
| 22 |
+
.from(repositories)
|
| 23 |
+
.where(eq(repositories.userId, user[0].id))
|
| 24 |
+
.orderBy(desc(repositories.createdAt));
|
| 25 |
+
|
| 26 |
+
return NextResponse.json(repos);
|
| 27 |
+
} catch (error) {
|
| 28 |
+
console.error("GET /api/profile/:username/repos error:", error);
|
| 29 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 30 |
+
}
|
| 31 |
+
}
|
src/app/api/profile/[username]/route.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { getProfileByUsername } from "@/lib/db/queries/users";
|
| 3 |
+
|
| 4 |
+
export async function GET(
|
| 5 |
+
request: NextRequest,
|
| 6 |
+
context: { params: Promise<{ username: string }> }
|
| 7 |
+
) {
|
| 8 |
+
try {
|
| 9 |
+
const { username } = await context.params;
|
| 10 |
+
const profile = await getProfileByUsername(username);
|
| 11 |
+
if (!profile) {
|
| 12 |
+
return NextResponse.json({ error: "Profile not found" }, { status: 404 });
|
| 13 |
+
}
|
| 14 |
+
return NextResponse.json(profile);
|
| 15 |
+
} catch (error) {
|
| 16 |
+
console.error("GET /api/profile/:username error:", error);
|
| 17 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 18 |
+
}
|
| 19 |
+
}
|
src/app/api/profile/route.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Profile Route
|
| 3 |
+
*
|
| 4 |
+
* Get and update user profiles.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 8 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 9 |
+
import { getProfileByUsername, createOrUpdateProfile } from "@/lib/db/queries/users";
|
| 10 |
+
|
| 11 |
+
export async function GET(request: NextRequest) {
|
| 12 |
+
try {
|
| 13 |
+
const { searchParams } = new URL(request.url);
|
| 14 |
+
const username = searchParams.get("username");
|
| 15 |
+
|
| 16 |
+
if (!username) {
|
| 17 |
+
return NextResponse.json({ error: "Username is required" }, { status: 400 });
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const profile = await getProfileByUsername(username);
|
| 21 |
+
if (!profile) {
|
| 22 |
+
return NextResponse.json({ error: "Profile not found" }, { status: 404 });
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
return NextResponse.json(profile);
|
| 26 |
+
} catch (error) {
|
| 27 |
+
console.error("Profile fetch error:", error);
|
| 28 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
export async function PUT(request: NextRequest) {
|
| 33 |
+
try {
|
| 34 |
+
const user = await getCurrentUser(request);
|
| 35 |
+
if (!user) {
|
| 36 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
const body = await request.json();
|
| 40 |
+
const { bio, location, website, twitter, skills, availableForMentoring, mentoringTopics } = body;
|
| 41 |
+
|
| 42 |
+
const profile = await createOrUpdateProfile(user.id, {
|
| 43 |
+
username: user.username,
|
| 44 |
+
avatarUrl: user.avatarUrl,
|
| 45 |
+
bio,
|
| 46 |
+
location,
|
| 47 |
+
website,
|
| 48 |
+
twitter,
|
| 49 |
+
skills,
|
| 50 |
+
availableForMentoring,
|
| 51 |
+
mentoringTopics,
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
return NextResponse.json(profile);
|
| 55 |
+
} catch (error) {
|
| 56 |
+
console.error("Profile update error:", error);
|
| 57 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 58 |
+
}
|
| 59 |
+
}
|
src/app/api/rag/chat/route.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* RAG Chat Route
|
| 3 |
+
*
|
| 4 |
+
* POST /api/rag/chat
|
| 5 |
+
* Answer questions using RAG (Retrieval-Augmented Generation)
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { ragChat } from "@/lib/ai-client";
|
| 11 |
+
|
| 12 |
+
export async function POST(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const body = await request.json();
|
| 20 |
+
const { question, repo_name, top_k } = body;
|
| 21 |
+
|
| 22 |
+
if (!question) {
|
| 23 |
+
return NextResponse.json({ error: "question is required" }, { status: 400 });
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const result = await ragChat(question, repo_name, top_k || 5);
|
| 27 |
+
|
| 28 |
+
if (!result.success) {
|
| 29 |
+
return NextResponse.json({ error: result.error }, { status: 502 });
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return NextResponse.json(result.data);
|
| 33 |
+
} catch (error) {
|
| 34 |
+
console.error("RAG chat error:", error);
|
| 35 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 36 |
+
}
|
| 37 |
+
}
|
src/app/api/rag/index/route.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* RAG Index Route
|
| 3 |
+
*
|
| 4 |
+
* POST /api/rag/index
|
| 5 |
+
* Index a repository for RAG search
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { ragIndex } from "@/lib/ai-client";
|
| 11 |
+
|
| 12 |
+
export async function POST(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const body = await request.json();
|
| 20 |
+
const { repo_name } = body;
|
| 21 |
+
|
| 22 |
+
if (!repo_name) {
|
| 23 |
+
return NextResponse.json({ error: "repo_name is required" }, { status: 400 });
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const result = await ragIndex(repo_name);
|
| 27 |
+
|
| 28 |
+
if (!result.success) {
|
| 29 |
+
return NextResponse.json({ error: result.error }, { status: 502 });
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return NextResponse.json(result.data);
|
| 33 |
+
} catch (error) {
|
| 34 |
+
console.error("RAG index error:", error);
|
| 35 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 36 |
+
}
|
| 37 |
+
}
|
src/app/api/rag/search/route.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* RAG Search Route
|
| 3 |
+
*
|
| 4 |
+
* POST /api/rag/search
|
| 5 |
+
* Search documents using RAG
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { ragSearch } from "@/lib/ai-client";
|
| 11 |
+
|
| 12 |
+
export async function POST(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const body = await request.json();
|
| 20 |
+
const { query, repo_name, limit } = body;
|
| 21 |
+
|
| 22 |
+
if (!query) {
|
| 23 |
+
return NextResponse.json({ error: "query is required" }, { status: 400 });
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const result = await ragSearch(query, repo_name, limit || 10);
|
| 27 |
+
|
| 28 |
+
if (!result.success) {
|
| 29 |
+
return NextResponse.json({ error: result.error }, { status: 502 });
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return NextResponse.json(result.data);
|
| 33 |
+
} catch (error) {
|
| 34 |
+
console.error("RAG search error:", error);
|
| 35 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 36 |
+
}
|
| 37 |
+
}
|
src/app/api/rag/suggestions/route.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* RAG Suggestions Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/rag/suggestions
|
| 5 |
+
* Get suggested questions for RAG chatbot
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { callAIEngine } from "@/lib/ai-client";
|
| 11 |
+
|
| 12 |
+
export async function GET(request: NextRequest) {
|
| 13 |
+
try {
|
| 14 |
+
const user = await getCurrentUser(request);
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const { searchParams } = new URL(request.url);
|
| 20 |
+
const repoName = searchParams.get("repo_name");
|
| 21 |
+
|
| 22 |
+
// Call AI engine with empty body for suggestions
|
| 23 |
+
const AI_ENGINE_URL = process.env.AI_ENGINE_URL || "http://localhost:7860";
|
| 24 |
+
const url = repoName
|
| 25 |
+
? `${AI_ENGINE_URL}/rag/suggestions?repo_name=${encodeURIComponent(repoName)}`
|
| 26 |
+
: `${AI_ENGINE_URL}/rag/suggestions`;
|
| 27 |
+
|
| 28 |
+
const response = await fetch(url);
|
| 29 |
+
|
| 30 |
+
if (!response.ok) {
|
| 31 |
+
return NextResponse.json({ error: "Failed to get suggestions" }, { status: 502 });
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const data = await response.json();
|
| 35 |
+
return NextResponse.json(data);
|
| 36 |
+
} catch (error) {
|
| 37 |
+
console.error("RAG suggestions error:", error);
|
| 38 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 39 |
+
}
|
| 40 |
+
}
|
src/app/api/repositories/contributor/route.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { db } from "@/db";
|
| 3 |
+
import { issues, repositories } from "@/db/schema";
|
| 4 |
+
import { eq, and, ne, desc, sql, count, max, inArray } from "drizzle-orm";
|
| 5 |
+
|
| 6 |
+
// =============================================================================
|
| 7 |
+
// GET /api/repositories/contributor - Get repos where user has PRs
|
| 8 |
+
// =============================================================================
|
| 9 |
+
//
|
| 10 |
+
// This is the most complex query in the original Python backend.
|
| 11 |
+
// It uses a MongoDB aggregation pipeline to find unique repos where
|
| 12 |
+
// the user has contributed PRs but is NOT the owner.
|
| 13 |
+
//
|
| 14 |
+
// Original Python aggregation pipeline (from routes/repository.py):
|
| 15 |
+
//
|
| 16 |
+
// pipeline = [
|
| 17 |
+
// {"$match": {"authorName": username, "isPR": True}},
|
| 18 |
+
// {"$group": {
|
| 19 |
+
// "_id": {"repoId": "$repoId", "repoName": "$repoName", "owner": "$owner", "repo": "$repo"},
|
| 20 |
+
// "pr_count": {"$sum": 1},
|
| 21 |
+
// "last_pr_at": {"$max": "$createdAt"}
|
| 22 |
+
// }},
|
| 23 |
+
// {"$project": {...}},
|
| 24 |
+
// {"$sort": {"pr_count": -1}}
|
| 25 |
+
// ]
|
| 26 |
+
// contributed_repos = await db.issues.aggregate(pipeline).to_list(1000)
|
| 27 |
+
//
|
| 28 |
+
// Drizzle SQL equivalent using GROUP BY:
|
| 29 |
+
//
|
| 30 |
+
export async function GET(request: NextRequest) {
|
| 31 |
+
try {
|
| 32 |
+
const { searchParams } = new URL(request.url);
|
| 33 |
+
const username = searchParams.get("username");
|
| 34 |
+
const userId = searchParams.get("userId");
|
| 35 |
+
|
| 36 |
+
if (!username || !userId) {
|
| 37 |
+
return NextResponse.json(
|
| 38 |
+
{ error: "username and userId are required" },
|
| 39 |
+
{ status: 400 }
|
| 40 |
+
);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// Step 1: Get user's own repo IDs to exclude
|
| 44 |
+
const ownRepos = await db
|
| 45 |
+
.select({ id: repositories.id })
|
| 46 |
+
.from(repositories)
|
| 47 |
+
.where(eq(repositories.userId, userId));
|
| 48 |
+
|
| 49 |
+
const ownRepoIds = ownRepos.map(r => r.id);
|
| 50 |
+
|
| 51 |
+
// Step 2: Aggregate PRs by repo - equivalent to MongoDB $group
|
| 52 |
+
// SQL: SELECT repo_id, repo_name, COUNT(*) as pr_count, MAX(created_at) as last_pr_at
|
| 53 |
+
// FROM issues WHERE author_name = ? AND is_pr = 1 GROUP BY repo_id, repo_name, owner, repo
|
| 54 |
+
const contributedRepos = await db
|
| 55 |
+
.select({
|
| 56 |
+
repoId: issues.repoId,
|
| 57 |
+
repoName: issues.repoName,
|
| 58 |
+
owner: issues.owner,
|
| 59 |
+
repo: issues.repo,
|
| 60 |
+
prCount: count().as("pr_count"),
|
| 61 |
+
lastPrAt: max(issues.createdAt).as("last_pr_at"),
|
| 62 |
+
})
|
| 63 |
+
.from(issues)
|
| 64 |
+
.where(
|
| 65 |
+
and(
|
| 66 |
+
eq(issues.authorName, username),
|
| 67 |
+
eq(issues.isPR, true)
|
| 68 |
+
)
|
| 69 |
+
)
|
| 70 |
+
.groupBy(issues.repoId, issues.repoName, issues.owner, issues.repo)
|
| 71 |
+
.orderBy(desc(sql`pr_count`));
|
| 72 |
+
|
| 73 |
+
// Step 3: Filter out user's own repos
|
| 74 |
+
const result = contributedRepos
|
| 75 |
+
.filter(repo => !ownRepoIds.includes(repo.repoId))
|
| 76 |
+
.map(repo => ({
|
| 77 |
+
repoId: repo.repoId,
|
| 78 |
+
repoName: repo.repoName,
|
| 79 |
+
owner: repo.owner,
|
| 80 |
+
repo: repo.repo,
|
| 81 |
+
pr_count: repo.prCount,
|
| 82 |
+
last_pr_at: repo.lastPrAt,
|
| 83 |
+
role: "contributor",
|
| 84 |
+
name: repo.repoName || `${repo.owner}/${repo.repo}`,
|
| 85 |
+
}));
|
| 86 |
+
|
| 87 |
+
return NextResponse.json(
|
| 88 |
+
{ repos: result, count: result.length },
|
| 89 |
+
{ status: 200 }
|
| 90 |
+
);
|
| 91 |
+
} catch (error) {
|
| 92 |
+
console.error("GET /api/repositories/contributor error:", error);
|
| 93 |
+
return NextResponse.json(
|
| 94 |
+
{ error: "Failed to fetch contributor repositories" },
|
| 95 |
+
{ status: 500 }
|
| 96 |
+
);
|
| 97 |
+
}
|
| 98 |
+
}
|
src/app/api/repositories/route.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { db } from "@/db";
|
| 3 |
+
import { repositories, issues, triageData } from "@/db/schema";
|
| 4 |
+
import { generateId, now } from "@/lib/utils";
|
| 5 |
+
import { eq, and, desc, count, sql } from "drizzle-orm";
|
| 6 |
+
|
| 7 |
+
// =============================================================================
|
| 8 |
+
// GET /api/repositories - Get user's repositories
|
| 9 |
+
// =============================================================================
|
| 10 |
+
//
|
| 11 |
+
// Python equivalent (from routes/repository.py):
|
| 12 |
+
// repos = await db.repositories.find({"userId": user['id']}, {"_id": 0}).to_list(1000)
|
| 13 |
+
//
|
| 14 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 15 |
+
|
| 16 |
+
// ...
|
| 17 |
+
|
| 18 |
+
export async function GET(request: NextRequest) {
|
| 19 |
+
try {
|
| 20 |
+
const { searchParams } = new URL(request.url);
|
| 21 |
+
let userId = searchParams.get("userId");
|
| 22 |
+
|
| 23 |
+
// If no userId provided, try to get the current authenticated user
|
| 24 |
+
if (!userId) {
|
| 25 |
+
const currentUser = await getCurrentUser(request);
|
| 26 |
+
if (currentUser) {
|
| 27 |
+
userId = currentUser.id;
|
| 28 |
+
} else {
|
| 29 |
+
return NextResponse.json(
|
| 30 |
+
{ error: "userId is required or you must be logged in" },
|
| 31 |
+
{ status: 401 }
|
| 32 |
+
);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const repos = await db
|
| 37 |
+
.select()
|
| 38 |
+
.from(repositories)
|
| 39 |
+
.where(eq(repositories.userId, userId))
|
| 40 |
+
.orderBy(desc(repositories.createdAt));
|
| 41 |
+
|
| 42 |
+
return NextResponse.json(repos, { status: 200 });
|
| 43 |
+
} catch (error) {
|
| 44 |
+
console.error("GET /api/repositories error:", error);
|
| 45 |
+
return NextResponse.json(
|
| 46 |
+
{ error: "Failed to fetch repositories" },
|
| 47 |
+
{ status: 500 }
|
| 48 |
+
);
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// =============================================================================
|
| 53 |
+
// POST /api/repositories - Add a new repository
|
| 54 |
+
// =============================================================================
|
| 55 |
+
//
|
| 56 |
+
// Python equivalent (from routes/repository.py):
|
| 57 |
+
// repository = Repository(
|
| 58 |
+
// githubRepoId=repo_data['id'],
|
| 59 |
+
// name=request.repoFullName,
|
| 60 |
+
// owner=owner,
|
| 61 |
+
// userId=user['id']
|
| 62 |
+
// )
|
| 63 |
+
// repo_dict = repository.model_dump()
|
| 64 |
+
// repo_dict['createdAt'] = repo_dict['createdAt'].isoformat()
|
| 65 |
+
// await db.repositories.insert_one(repo_dict)
|
| 66 |
+
//
|
| 67 |
+
export async function POST(request: NextRequest) {
|
| 68 |
+
try {
|
| 69 |
+
const body = await request.json();
|
| 70 |
+
const { githubRepoId, name, owner, userId } = body;
|
| 71 |
+
|
| 72 |
+
// Validate required fields
|
| 73 |
+
if (!githubRepoId || !name || !owner || !userId) {
|
| 74 |
+
return NextResponse.json(
|
| 75 |
+
{ error: "Missing required fields: githubRepoId, name, owner, userId" },
|
| 76 |
+
{ status: 400 }
|
| 77 |
+
);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Check if repository already exists for this user
|
| 81 |
+
const existing = await db
|
| 82 |
+
.select()
|
| 83 |
+
.from(repositories)
|
| 84 |
+
.where(
|
| 85 |
+
and(
|
| 86 |
+
eq(repositories.githubRepoId, githubRepoId),
|
| 87 |
+
eq(repositories.userId, userId)
|
| 88 |
+
)
|
| 89 |
+
)
|
| 90 |
+
.limit(1);
|
| 91 |
+
|
| 92 |
+
if (existing.length > 0) {
|
| 93 |
+
return NextResponse.json(
|
| 94 |
+
{ error: "Repository already added", repository: existing[0] },
|
| 95 |
+
{ status: 409 }
|
| 96 |
+
);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Create new repository
|
| 100 |
+
const newRepo = {
|
| 101 |
+
id: generateId(),
|
| 102 |
+
githubRepoId,
|
| 103 |
+
name,
|
| 104 |
+
owner,
|
| 105 |
+
userId,
|
| 106 |
+
createdAt: now(),
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
await db.insert(repositories).values(newRepo);
|
| 110 |
+
|
| 111 |
+
return NextResponse.json(
|
| 112 |
+
{ message: "Repository added!", repository: newRepo },
|
| 113 |
+
{ status: 201 }
|
| 114 |
+
);
|
| 115 |
+
} catch (error) {
|
| 116 |
+
console.error("POST /api/repositories error:", error);
|
| 117 |
+
return NextResponse.json(
|
| 118 |
+
{ error: "Failed to add repository" },
|
| 119 |
+
{ status: 500 }
|
| 120 |
+
);
|
| 121 |
+
}
|
| 122 |
+
}
|
src/app/api/spark/badges/user/[username]/route.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { getUserBadges } from "@/lib/db/queries/gamification";
|
| 3 |
+
|
| 4 |
+
export async function GET(
|
| 5 |
+
request: NextRequest,
|
| 6 |
+
context: { params: Promise<{ username: string }> }
|
| 7 |
+
) {
|
| 8 |
+
try {
|
| 9 |
+
const { username } = await context.params;
|
| 10 |
+
const badges = await getUserBadges(username);
|
| 11 |
+
return NextResponse.json(badges);
|
| 12 |
+
} catch (error) {
|
| 13 |
+
console.error("GET /api/spark/badges/user/:username error:", error);
|
| 14 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 15 |
+
}
|
| 16 |
+
}
|
src/app/api/spark/gamification/calendar/[username]/route.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { getUserCalendar } from "@/lib/db/queries/gamification";
|
| 3 |
+
|
| 4 |
+
export async function GET(
|
| 5 |
+
request: NextRequest,
|
| 6 |
+
context: { params: Promise<{ username: string }> }
|
| 7 |
+
) {
|
| 8 |
+
try {
|
| 9 |
+
const { username } = await context.params;
|
| 10 |
+
const calendar = await getUserCalendar(username);
|
| 11 |
+
return NextResponse.json(calendar);
|
| 12 |
+
} catch (error) {
|
| 13 |
+
console.error("GET /api/spark/gamification/calendar/:username error:", error);
|
| 14 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 15 |
+
}
|
| 16 |
+
}
|
src/app/api/spark/gamification/streak/[username]/route.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { getUserStreak } from "@/lib/db/queries/gamification";
|
| 3 |
+
|
| 4 |
+
export async function GET(
|
| 5 |
+
request: NextRequest,
|
| 6 |
+
context: { params: Promise<{ username: string }> }
|
| 7 |
+
) {
|
| 8 |
+
try {
|
| 9 |
+
const { username } = await context.params;
|
| 10 |
+
const streak = await getUserStreak(username);
|
| 11 |
+
return NextResponse.json(streak);
|
| 12 |
+
} catch (error) {
|
| 13 |
+
console.error("GET /api/spark/gamification/streak/:username error:", error);
|
| 14 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 15 |
+
}
|
| 16 |
+
}
|
src/app/api/triage/route.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { db } from "@/db";
|
| 3 |
+
import { issues, triageData } from "@/db/schema";
|
| 4 |
+
import { requireAuth } from "@/lib/auth";
|
| 5 |
+
import { generateId, now } from "@/lib/utils";
|
| 6 |
+
import { eq } from "drizzle-orm";
|
| 7 |
+
|
| 8 |
+
// OpenRouter API for AI classification
|
| 9 |
+
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY!;
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Classifications matching the Python backend
|
| 13 |
+
*/
|
| 14 |
+
const CLASSIFICATIONS = [
|
| 15 |
+
"CRITICAL_BUG", "BUG", "FEATURE_REQUEST", "QUESTION",
|
| 16 |
+
"DOCS", "DUPLICATE", "NEEDS_INFO", "SPAM"
|
| 17 |
+
] as const;
|
| 18 |
+
|
| 19 |
+
const SENTIMENTS = ["POSITIVE", "NEUTRAL", "NEGATIVE", "FRUSTRATED"] as const;
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* POST /api/triage
|
| 23 |
+
* Classify an issue using AI.
|
| 24 |
+
*
|
| 25 |
+
* Request body: { issueId: string }
|
| 26 |
+
*/
|
| 27 |
+
export async function POST(request: NextRequest) {
|
| 28 |
+
const { user, error } = await requireAuth(request);
|
| 29 |
+
if (error) return error;
|
| 30 |
+
|
| 31 |
+
try {
|
| 32 |
+
const body = await request.json();
|
| 33 |
+
const { issueId } = body;
|
| 34 |
+
|
| 35 |
+
if (!issueId) {
|
| 36 |
+
return NextResponse.json(
|
| 37 |
+
{ error: "issueId is required" },
|
| 38 |
+
{ status: 400 }
|
| 39 |
+
);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Fetch the issue
|
| 43 |
+
const issueRecords = await db
|
| 44 |
+
.select()
|
| 45 |
+
.from(issues)
|
| 46 |
+
.where(eq(issues.id, issueId))
|
| 47 |
+
.limit(1);
|
| 48 |
+
|
| 49 |
+
if (issueRecords.length === 0) {
|
| 50 |
+
return NextResponse.json(
|
| 51 |
+
{ error: "Issue not found" },
|
| 52 |
+
{ status: 404 }
|
| 53 |
+
);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
const issue = issueRecords[0];
|
| 57 |
+
|
| 58 |
+
// Check if already triaged
|
| 59 |
+
const existingTriage = await db
|
| 60 |
+
.select()
|
| 61 |
+
.from(triageData)
|
| 62 |
+
.where(eq(triageData.issueId, issueId))
|
| 63 |
+
.limit(1);
|
| 64 |
+
|
| 65 |
+
if (existingTriage.length > 0) {
|
| 66 |
+
return NextResponse.json({
|
| 67 |
+
message: "Issue already triaged",
|
| 68 |
+
triage: existingTriage[0],
|
| 69 |
+
});
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Build AI prompt for classification
|
| 73 |
+
const prompt = `Analyze this GitHub issue and provide a classification.
|
| 74 |
+
|
| 75 |
+
Title: ${issue.title}
|
| 76 |
+
Body: ${issue.body || "(empty)"}
|
| 77 |
+
|
| 78 |
+
Respond with valid JSON only:
|
| 79 |
+
{
|
| 80 |
+
"classification": "BUG" | "CRITICAL_BUG" | "FEATURE_REQUEST" | "QUESTION" | "DOCS" | "DUPLICATE" | "NEEDS_INFO" | "SPAM",
|
| 81 |
+
"sentiment": "POSITIVE" | "NEUTRAL" | "NEGATIVE" | "FRUSTRATED",
|
| 82 |
+
"summary": "One-line summary of the issue",
|
| 83 |
+
"suggestedLabel": "Suggested GitHub label (lowercase, hyphenated)"
|
| 84 |
+
}`;
|
| 85 |
+
|
| 86 |
+
// Call OpenRouter API
|
| 87 |
+
const aiResponse = await fetch(
|
| 88 |
+
"https://openrouter.ai/api/v1/chat/completions",
|
| 89 |
+
{
|
| 90 |
+
method: "POST",
|
| 91 |
+
headers: {
|
| 92 |
+
Authorization: `Bearer ${OPENROUTER_API_KEY}`,
|
| 93 |
+
"Content-Type": "application/json",
|
| 94 |
+
},
|
| 95 |
+
body: JSON.stringify({
|
| 96 |
+
model: "anthropic/claude-3-haiku",
|
| 97 |
+
messages: [{ role: "user", content: prompt }],
|
| 98 |
+
max_tokens: 500,
|
| 99 |
+
}),
|
| 100 |
+
}
|
| 101 |
+
);
|
| 102 |
+
|
| 103 |
+
const aiData = await aiResponse.json();
|
| 104 |
+
const aiContent = aiData.choices?.[0]?.message?.content || "{}";
|
| 105 |
+
|
| 106 |
+
// Parse AI response
|
| 107 |
+
let classification = "NEEDS_INFO";
|
| 108 |
+
let sentiment = "NEUTRAL";
|
| 109 |
+
let summary = issue.title;
|
| 110 |
+
let suggestedLabel = "needs-triage";
|
| 111 |
+
|
| 112 |
+
try {
|
| 113 |
+
const parsed = JSON.parse(aiContent);
|
| 114 |
+
classification = CLASSIFICATIONS.includes(parsed.classification)
|
| 115 |
+
? parsed.classification
|
| 116 |
+
: "NEEDS_INFO";
|
| 117 |
+
sentiment = SENTIMENTS.includes(parsed.sentiment)
|
| 118 |
+
? parsed.sentiment
|
| 119 |
+
: "NEUTRAL";
|
| 120 |
+
summary = parsed.summary || issue.title;
|
| 121 |
+
suggestedLabel = parsed.suggestedLabel || "needs-triage";
|
| 122 |
+
} catch {
|
| 123 |
+
console.error("Failed to parse AI response:", aiContent);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// Save triage data
|
| 127 |
+
const triage = {
|
| 128 |
+
id: generateId(),
|
| 129 |
+
issueId,
|
| 130 |
+
classification,
|
| 131 |
+
summary,
|
| 132 |
+
suggestedLabel,
|
| 133 |
+
sentiment,
|
| 134 |
+
analyzedAt: now(),
|
| 135 |
+
};
|
| 136 |
+
|
| 137 |
+
await db.insert(triageData).values(triage);
|
| 138 |
+
|
| 139 |
+
return NextResponse.json({
|
| 140 |
+
message: "Issue triaged successfully",
|
| 141 |
+
triage,
|
| 142 |
+
});
|
| 143 |
+
} catch (error) {
|
| 144 |
+
console.error("POST /api/triage error:", error);
|
| 145 |
+
return NextResponse.json(
|
| 146 |
+
{ error: "Failed to triage issue" },
|
| 147 |
+
{ status: 500 }
|
| 148 |
+
);
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/**
|
| 153 |
+
* GET /api/triage?issueId=xxx
|
| 154 |
+
* Get triage data for an issue.
|
| 155 |
+
*/
|
| 156 |
+
export async function GET(request: NextRequest) {
|
| 157 |
+
const { searchParams } = new URL(request.url);
|
| 158 |
+
const issueId = searchParams.get("issueId");
|
| 159 |
+
|
| 160 |
+
if (!issueId) {
|
| 161 |
+
return NextResponse.json(
|
| 162 |
+
{ error: "issueId is required" },
|
| 163 |
+
{ status: 400 }
|
| 164 |
+
);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
const triageRecords = await db
|
| 168 |
+
.select()
|
| 169 |
+
.from(triageData)
|
| 170 |
+
.where(eq(triageData.issueId, issueId))
|
| 171 |
+
.limit(1);
|
| 172 |
+
|
| 173 |
+
if (triageRecords.length === 0) {
|
| 174 |
+
return NextResponse.json(
|
| 175 |
+
{ error: "Triage data not found" },
|
| 176 |
+
{ status: 404 }
|
| 177 |
+
);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
return NextResponse.json(triageRecords[0]);
|
| 181 |
+
}
|
src/app/favicon.ico
ADDED
|
|
src/app/globals.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--background: #ffffff;
|
| 5 |
+
--foreground: #171717;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
@theme inline {
|
| 9 |
+
--color-background: var(--background);
|
| 10 |
+
--color-foreground: var(--foreground);
|
| 11 |
+
--font-sans: var(--font-geist-sans);
|
| 12 |
+
--font-mono: var(--font-geist-mono);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
@media (prefers-color-scheme: dark) {
|
| 16 |
+
:root {
|
| 17 |
+
--background: #0a0a0a;
|
| 18 |
+
--foreground: #ededed;
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
body {
|
| 23 |
+
background: var(--background);
|
| 24 |
+
color: var(--foreground);
|
| 25 |
+
font-family: Arial, Helvetica, sans-serif;
|
| 26 |
+
}
|
src/app/layout.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Geist, Geist_Mono } from "next/font/google";
|
| 3 |
+
import "./globals.css";
|
| 4 |
+
|
| 5 |
+
const geistSans = Geist({
|
| 6 |
+
variable: "--font-geist-sans",
|
| 7 |
+
subsets: ["latin"],
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
+
const geistMono = Geist_Mono({
|
| 11 |
+
variable: "--font-geist-mono",
|
| 12 |
+
subsets: ["latin"],
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
export const metadata: Metadata = {
|
| 16 |
+
title: "Create Next App",
|
| 17 |
+
description: "Generated by create next app",
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
export default function RootLayout({
|
| 21 |
+
children,
|
| 22 |
+
}: Readonly<{
|
| 23 |
+
children: React.ReactNode;
|
| 24 |
+
}>) {
|
| 25 |
+
return (
|
| 26 |
+
<html lang="en">
|
| 27 |
+
<body
|
| 28 |
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
| 29 |
+
>
|
| 30 |
+
{children}
|
| 31 |
+
</body>
|
| 32 |
+
</html>
|
| 33 |
+
);
|
| 34 |
+
}
|
src/app/page.tsx
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Image from "next/image";
|
| 2 |
+
|
| 3 |
+
export default function Home() {
|
| 4 |
+
return (
|
| 5 |
+
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
| 6 |
+
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
| 7 |
+
<Image
|
| 8 |
+
className="dark:invert"
|
| 9 |
+
src="/next.svg"
|
| 10 |
+
alt="Next.js logo"
|
| 11 |
+
width={100}
|
| 12 |
+
height={20}
|
| 13 |
+
priority
|
| 14 |
+
/>
|
| 15 |
+
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
| 16 |
+
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
| 17 |
+
To get started, edit the page.tsx file.
|
| 18 |
+
</h1>
|
| 19 |
+
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
| 20 |
+
Looking for a starting point or more instructions? Head over to{" "}
|
| 21 |
+
<a
|
| 22 |
+
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 23 |
+
className="font-medium text-zinc-950 dark:text-zinc-50"
|
| 24 |
+
>
|
| 25 |
+
Templates
|
| 26 |
+
</a>{" "}
|
| 27 |
+
or the{" "}
|
| 28 |
+
<a
|
| 29 |
+
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 30 |
+
className="font-medium text-zinc-950 dark:text-zinc-50"
|
| 31 |
+
>
|
| 32 |
+
Learning
|
| 33 |
+
</a>{" "}
|
| 34 |
+
center.
|
| 35 |
+
</p>
|
| 36 |
+
</div>
|
| 37 |
+
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
| 38 |
+
<a
|
| 39 |
+
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
| 40 |
+
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 41 |
+
target="_blank"
|
| 42 |
+
rel="noopener noreferrer"
|
| 43 |
+
>
|
| 44 |
+
<Image
|
| 45 |
+
className="dark:invert"
|
| 46 |
+
src="/vercel.svg"
|
| 47 |
+
alt="Vercel logomark"
|
| 48 |
+
width={16}
|
| 49 |
+
height={16}
|
| 50 |
+
/>
|
| 51 |
+
Deploy Now
|
| 52 |
+
</a>
|
| 53 |
+
<a
|
| 54 |
+
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
| 55 |
+
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
| 56 |
+
target="_blank"
|
| 57 |
+
rel="noopener noreferrer"
|
| 58 |
+
>
|
| 59 |
+
Documentation
|
| 60 |
+
</a>
|
| 61 |
+
</div>
|
| 62 |
+
</main>
|
| 63 |
+
</div>
|
| 64 |
+
);
|
| 65 |
+
}
|