Spaces:
Sleeping
Sleeping
Commit ·
c7355e0
1
Parent(s): 117459b
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 +0 -949
- src/app/api/ai/chat/route.ts +0 -41
- src/app/api/ai/mentor-match/route.ts +0 -32
- src/app/api/ai/rag/route.ts +0 -36
- src/app/api/ai/triage/route.ts +0 -41
- src/app/api/auth/github/callback/route.ts +0 -101
- src/app/api/auth/github/route.ts +0 -15
- src/app/api/auth/me/route.ts +0 -22
- src/app/api/auth/select-role/route.ts +0 -51
- src/app/api/chat/route.ts +0 -43
- src/app/api/contributor/claim-activity/[issueId]/route.ts +0 -32
- src/app/api/contributor/claim-issue/[issueId]/route.ts +0 -32
- src/app/api/contributor/claim-issue/route.ts +0 -38
- src/app/api/contributor/dashboard-summary/route.ts +0 -44
- src/app/api/contributor/my-claimed-issues/route.ts +0 -27
- src/app/api/contributor/my-issues/route.ts +0 -36
- src/app/api/contributor/route.ts +0 -54
- src/app/api/issues/[id]/messages/route.ts +0 -139
- src/app/api/issues/route.ts +0 -142
- src/app/api/maintainer/dashboard-summary/route.ts +0 -39
- src/app/api/maintainer/issues/route.ts +0 -44
- src/app/api/maintainer/route.ts +0 -49
- src/app/api/maintainer/templates/route.ts +0 -25
- src/app/api/messages/route.ts +0 -63
- src/app/api/messaging/conversations/route.ts +0 -36
- src/app/api/messaging/history/[userId]/route.ts +0 -35
- src/app/api/messaging/mark-read/[userId]/route.ts +0 -31
- src/app/api/messaging/poll/[userId]/route.ts +0 -33
- src/app/api/messaging/route.ts +0 -34
- src/app/api/messaging/send/route.ts +0 -40
- src/app/api/messaging/unread-count/route.ts +0 -26
- src/app/api/profile/[id]/connected-repos/route.ts +0 -23
- src/app/api/profile/[username]/featured-badges/route.ts +0 -20
- src/app/api/profile/[username]/repos/route.ts +0 -31
- src/app/api/profile/[username]/route.ts +0 -19
- src/app/api/profile/route.ts +0 -59
- src/app/api/rag/chat/route.ts +0 -37
- src/app/api/rag/index/route.ts +0 -37
- src/app/api/rag/search/route.ts +0 -37
- src/app/api/rag/suggestions/route.ts +0 -40
- src/app/api/repositories/contributor/route.ts +0 -98
- src/app/api/repositories/route.ts +0 -122
- src/app/api/spark/badges/user/[username]/route.ts +0 -16
- src/app/api/spark/gamification/calendar/[username]/route.ts +0 -16
- src/app/api/spark/gamification/streak/[username]/route.ts +0 -16
- src/app/api/triage/route.ts +0 -181
- src/app/favicon.ico +0 -0
- src/app/globals.css +0 -26
- src/app/layout.tsx +0 -34
- src/app/page.tsx +0 -65
scripts/migrate-data.ts
DELETED
|
@@ -1,949 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,43 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,54 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,139 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,142 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,49 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,98 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,122 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,181 +0,0 @@
|
|
| 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
DELETED
|
Binary file (25.9 kB)
|
|
|
src/app/globals.css
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|