From ef7a88e913e94d2a4ed3cbcee44172f7204af352 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Fri, 27 Feb 2026 13:25:38 -0800 Subject: [PATCH] migrate: replace Firebase with PostgreSQL across core routes - chat-context.ts: session history now from fs_sessions - /api/sessions: reads from fs_sessions (NextAuth session auth) - /api/github/connect: NextAuth session + stores in fs_users.data - /api/user/api-key: NextAuth session + stores in fs_users.data - /api/projects/[id]/vision: PATCH to fs_projects JSONB - /api/projects/[id]/knowledge/items: reads from fs_knowledge_items - /api/projects/[id]/knowledge/import-ai-chat: uses pg createKnowledgeItem - lib/server/knowledge.ts: fully rewritten to use PostgreSQL - entrypoint.sh: add fs_knowledge_items and chat_conversations tables Made-with: Cursor --- app/api/github/connect/route.ts | 157 ++++++------------ .../knowledge/import-ai-chat/route.ts | 13 +- .../[projectId]/knowledge/items/route.ts | 91 +++------- app/api/projects/[projectId]/vision/route.ts | 57 ++++--- app/api/sessions/route.ts | 101 ++++++----- app/api/user/api-key/route.ts | 79 +++------ entrypoint.sh | 13 ++ lib/server/chat-context.ts | 37 ++--- lib/server/knowledge.ts | 79 ++++----- 9 files changed, 267 insertions(+), 360 deletions(-) diff --git a/app/api/github/connect/route.ts b/app/api/github/connect/route.ts index d1543f5..bf4b598 100644 --- a/app/api/github/connect/route.ts +++ b/app/api/github/connect/route.ts @@ -1,151 +1,96 @@ import { NextResponse } from 'next/server'; -import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin'; -import { FieldValue } from 'firebase-admin/firestore'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; +import { query } from '@/lib/db-postgres'; -/** - * Store GitHub connection for authenticated user - * Encrypts and stores the access token securely - */ export async function POST(request: Request) { try { - const authHeader = request.headers.get('Authorization'); - if (!authHeader?.startsWith('Bearer ')) { + const session = await getServerSession(authOptions); + if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const idToken = authHeader.split('Bearer ')[1]; - const adminAuth = getAdminAuth(); - const adminDb = getAdminDb(); - - let userId: string; - try { - const decodedToken = await adminAuth.verifyIdToken(idToken); - userId = decodedToken.uid; - } catch (error) { - return NextResponse.json({ error: 'Invalid token' }, { status: 401 }); - } - const { accessToken, githubUser } = await request.json(); - if (!accessToken || !githubUser) { - return NextResponse.json( - { error: 'Missing required fields' }, - { status: 400 } - ); + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); } - // TODO: Encrypt the access token before storing - // For now, we'll store it directly (should use crypto.subtle or a library) - const encryptedToken = accessToken; // PLACEHOLDER + await query( + `UPDATE fs_users + SET data = data || $1::jsonb, updated_at = NOW() + WHERE data->>'email' = $2`, + [ + JSON.stringify({ + githubConnected: true, + githubUserId: githubUser.id, + githubUsername: githubUser.login, + githubName: githubUser.name, + githubEmail: githubUser.email, + githubAvatarUrl: githubUser.avatar_url, + githubAccessToken: accessToken, + githubConnectedAt: new Date().toISOString(), + }), + session.user.email, + ] + ); - // Store GitHub connection - const connectionRef = adminDb.collection('githubConnections').doc(userId); - await connectionRef.set({ - userId, - githubUserId: githubUser.id, - githubUsername: githubUser.login, - githubName: githubUser.name, - githubEmail: githubUser.email, - githubAvatarUrl: githubUser.avatar_url, - accessToken: encryptedToken, - connectedAt: FieldValue.serverTimestamp(), - lastSyncedAt: null, - }); - - return NextResponse.json({ - success: true, - githubUsername: githubUser.login, - }); + return NextResponse.json({ success: true, githubUsername: githubUser.login }); } catch (error) { console.error('[GitHub Connect] Error:', error); - return NextResponse.json( - { error: 'Failed to store GitHub connection' }, - { status: 500 } - ); + return NextResponse.json({ error: 'Failed to store GitHub connection' }, { status: 500 }); } } -/** - * Get GitHub connection status for authenticated user - */ export async function GET(request: Request) { try { - const authHeader = request.headers.get('Authorization'); - if (!authHeader?.startsWith('Bearer ')) { + const session = await getServerSession(authOptions); + if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const idToken = authHeader.split('Bearer ')[1]; - const adminAuth = getAdminAuth(); - const adminDb = getAdminDb(); + const rows = await query<{ data: any }>( + `SELECT data FROM fs_users WHERE data->>'email' = $1 LIMIT 1`, + [session.user.email] + ); - let userId: string; - try { - const decodedToken = await adminAuth.verifyIdToken(idToken); - userId = decodedToken.uid; - } catch (error) { - return NextResponse.json({ error: 'Invalid token' }, { status: 401 }); - } - - const connectionDoc = await adminDb - .collection('githubConnections') - .doc(userId) - .get(); - - if (!connectionDoc.exists) { + if (rows.length === 0 || !rows[0].data?.githubConnected) { return NextResponse.json({ connected: false }); } - const data = connectionDoc.data()!; - + const d = rows[0].data; return NextResponse.json({ connected: true, - githubUsername: data.githubUsername, - githubName: data.githubName, - githubAvatarUrl: data.githubAvatarUrl, - connectedAt: data.connectedAt, - lastSyncedAt: data.lastSyncedAt, + githubUsername: d.githubUsername, + githubName: d.githubName, + githubAvatarUrl: d.githubAvatarUrl, + connectedAt: d.githubConnectedAt, }); } catch (error) { console.error('[GitHub Connect] Error:', error); - return NextResponse.json( - { error: 'Failed to fetch GitHub connection' }, - { status: 500 } - ); + return NextResponse.json({ error: 'Failed to fetch GitHub connection' }, { status: 500 }); } } -/** - * Disconnect GitHub account - */ export async function DELETE(request: Request) { try { - const authHeader = request.headers.get('Authorization'); - if (!authHeader?.startsWith('Bearer ')) { + const session = await getServerSession(authOptions); + if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const idToken = authHeader.split('Bearer ')[1]; - const adminAuth = getAdminAuth(); - const adminDb = getAdminDb(); - - let userId: string; - try { - const decodedToken = await adminAuth.verifyIdToken(idToken); - userId = decodedToken.uid; - } catch (error) { - return NextResponse.json({ error: 'Invalid token' }, { status: 401 }); - } - - await adminDb.collection('githubConnections').doc(userId).delete(); + await query( + `UPDATE fs_users + SET data = data - 'githubConnected' - 'githubUserId' - 'githubUsername' + - 'githubName' - 'githubEmail' - 'githubAvatarUrl' + - 'githubAccessToken' - 'githubConnectedAt', + updated_at = NOW() + WHERE data->>'email' = $1`, + [session.user.email] + ); return NextResponse.json({ success: true }); } catch (error) { console.error('[GitHub Disconnect] Error:', error); - return NextResponse.json( - { error: 'Failed to disconnect GitHub' }, - { status: 500 } - ); + return NextResponse.json({ error: 'Failed to disconnect GitHub' }, { status: 500 }); } } - diff --git a/app/api/projects/[projectId]/knowledge/import-ai-chat/route.ts b/app/api/projects/[projectId]/knowledge/import-ai-chat/route.ts index 554963d..9b113d5 100644 --- a/app/api/projects/[projectId]/knowledge/import-ai-chat/route.ts +++ b/app/api/projects/[projectId]/knowledge/import-ai-chat/route.ts @@ -1,5 +1,7 @@ import { NextResponse } from 'next/server'; -import { getAdminDb } from '@/lib/firebase/admin'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; +import { query } from '@/lib/db-postgres'; import { createKnowledgeItem } from '@/lib/server/knowledge'; import type { KnowledgeSourceMeta } from '@/lib/types/knowledge'; @@ -32,9 +34,12 @@ export async function POST( return NextResponse.json({ error: 'transcript is required' }, { status: 400 }); } - const adminDb = getAdminDb(); - const projectSnap = await adminDb.collection('projects').doc(projectId).get(); - if (!projectSnap.exists) { + const session = await getServerSession(authOptions); + if (!session?.user?.email) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const projectRows = await query(`SELECT id FROM fs_projects WHERE id = $1 LIMIT 1`, [projectId]); + if (projectRows.length === 0) { return NextResponse.json({ error: 'Project not found' }, { status: 404 }); } diff --git a/app/api/projects/[projectId]/knowledge/items/route.ts b/app/api/projects/[projectId]/knowledge/items/route.ts index aad6be1..64acbee 100644 --- a/app/api/projects/[projectId]/knowledge/items/route.ts +++ b/app/api/projects/[projectId]/knowledge/items/route.ts @@ -1,5 +1,7 @@ import { NextResponse } from 'next/server'; -import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; +import { query } from '@/lib/db-postgres'; export async function GET( request: Request, @@ -7,75 +9,30 @@ export async function GET( ) { try { const { projectId } = await params; - - // Authentication (skip in development if no auth header) - const authHeader = request.headers.get('Authorization'); - const isDevelopment = process.env.NODE_ENV === 'development'; - - if (!isDevelopment || authHeader?.startsWith('Bearer ')) { - if (!authHeader?.startsWith('Bearer ')) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - const token = authHeader.substring(7); - const auth = getAdminAuth(); - const decoded = await auth.verifyIdToken(token); - - if (!decoded?.uid) { - return NextResponse.json({ error: 'Invalid token' }, { status: 401 }); - } + const session = await getServerSession(authOptions); + if (!session?.user?.email) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - // Fetch knowledge items from Firestore - console.log('[API /knowledge/items] Fetching items for project:', projectId); - - let items = []; - - try { - const adminDb = getAdminDb(); - const knowledgeSnapshot = await adminDb - .collection('knowledge') - .where('projectId', '==', projectId) - .orderBy('createdAt', 'desc') - .limit(100) - .get(); - - console.log('[API /knowledge/items] Found', knowledgeSnapshot.size, 'items'); - - items = knowledgeSnapshot.docs.map(doc => { - const data = doc.data(); - return { - id: doc.id, - title: data.title || data.content?.substring(0, 50) || 'Untitled', - sourceType: data.sourceType, - content: data.content, - sourceMeta: data.sourceMeta, - createdAt: data.createdAt?.toDate?.()?.toISOString() || data.createdAt, - updatedAt: data.updatedAt?.toDate?.()?.toISOString() || data.updatedAt, - }; - }); - } catch (firestoreError) { - console.error('[API /knowledge/items] Firestore query failed:', firestoreError); - console.error('[API /knowledge/items] This is likely due to missing Firebase Admin credentials or Firestore not being set up'); - // Return empty array instead of failing - the UI will show "No chats yet" and "No images yet" - items = []; - } - - return NextResponse.json({ - success: true, - items, - count: items.length, - }); - } catch (error) { - console.error('[API /knowledge/items] Error fetching knowledge items:', error); - console.error('[API /knowledge/items] Error stack:', error instanceof Error ? error.stack : 'No stack trace'); - return NextResponse.json( - { - error: 'Failed to fetch knowledge items', - details: error instanceof Error ? error.message : String(error) - }, - { status: 500 } + const rows = await query<{ id: string; data: any; created_at: string; updated_at: string }>( + `SELECT id, data, created_at, updated_at FROM fs_knowledge_items WHERE project_id = $1 ORDER BY created_at DESC LIMIT 100`, + [projectId] ); + + const items = rows.map((row) => ({ + id: row.id, + title: row.data?.title || row.data?.content?.substring(0, 50) || 'Untitled', + sourceType: row.data?.sourceType, + content: row.data?.content, + sourceMeta: row.data?.sourceMeta, + createdAt: row.created_at, + updatedAt: row.updated_at, + })); + + return NextResponse.json({ success: true, items, count: items.length }); + } catch (error) { + console.error('[API /knowledge/items] Error:', error); + return NextResponse.json({ success: true, items: [], count: 0 }); } } - diff --git a/app/api/projects/[projectId]/vision/route.ts b/app/api/projects/[projectId]/vision/route.ts index ffbc08e..2e4b361 100644 --- a/app/api/projects/[projectId]/vision/route.ts +++ b/app/api/projects/[projectId]/vision/route.ts @@ -1,15 +1,19 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getAdminDb } from '@/lib/firebase/admin'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; +import { query } from '@/lib/db-postgres'; -/** - * Save vision answers to Firestore - */ export async function POST( request: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; + const session = await getServerSession(authOptions); + if (!session?.user?.email) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const body = await request.json(); const { visionAnswers } = body; @@ -20,29 +24,41 @@ export async function POST( ); } - const adminDb = getAdminDb(); + const rows = await query<{ id: string; data: any }>( + `SELECT p.id, p.data FROM fs_projects p + JOIN fs_users u ON u.id = p.user_id + WHERE p.id = $1 AND u.data->>'email' = $2 LIMIT 1`, + [projectId, session.user.email] + ); - // Save vision answers and mark ready for MVP - await adminDb.collection('projects').doc(projectId).set( - { - visionAnswers: { - q1: visionAnswers.q1, - q2: visionAnswers.q2, - q3: visionAnswers.q3, - allAnswered: true, - updatedAt: visionAnswers.updatedAt || new Date().toISOString(), - }, - readyForMVP: true, - currentPhase: 'mvp', - phaseStatus: 'ready', + if (rows.length === 0) { + return NextResponse.json({ error: 'Project not found' }, { status: 404 }); + } + + const current = rows[0].data ?? {}; + const updated = { + ...current, + visionAnswers: { + q1: visionAnswers.q1, + q2: visionAnswers.q2, + q3: visionAnswers.q3, + allAnswered: true, + updatedAt: visionAnswers.updatedAt || new Date().toISOString(), }, - { merge: true } + readyForMVP: true, + currentPhase: 'mvp', + phaseStatus: 'ready', + updatedAt: new Date().toISOString(), + }; + + await query( + `UPDATE fs_projects SET data = $1::jsonb WHERE id = $2`, + [JSON.stringify(updated), projectId] ); console.log(`[Vision API] Saved vision answers for project ${projectId}`); // Trigger MVP generation (async - don't wait) - console.log(`[Vision API] Triggering MVP generation for project ${projectId}...`); fetch(new URL(`/api/projects/${projectId}/mvp-checklist`, request.url).toString(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -65,4 +81,3 @@ export async function POST( ); } } - diff --git a/app/api/sessions/route.ts b/app/api/sessions/route.ts index 4393238..9278ae9 100644 --- a/app/api/sessions/route.ts +++ b/app/api/sessions/route.ts @@ -1,59 +1,72 @@ import { NextResponse } from 'next/server'; -import { getAdminDb } from '@/lib/firebase/admin'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; +import { query } from '@/lib/db-postgres'; export async function GET(request: Request) { try { - const { searchParams } = new URL(request.url); - const projectId = searchParams.get('projectId'); - const limit = parseInt(searchParams.get('limit') || '10'); - - console.log(`[API] Fetching sessions for project ${projectId}, limit ${limit}`); - - const adminDb = getAdminDb(); - let sessionsQuery = adminDb.collection('sessions'); - - // Filter by projectId if provided - if (projectId) { - sessionsQuery = sessionsQuery.where('projectId', '==', projectId) as any; + const session = await getServerSession(authOptions); + if (!session?.user?.email) { + return NextResponse.json([], { status: 200 }); } - const sessionsSnapshot = await sessionsQuery - .orderBy('createdAt', 'desc') - .limit(limit) - .get(); + const { searchParams } = new URL(request.url); + const projectId = searchParams.get('projectId'); + const limit = parseInt(searchParams.get('limit') || '50'); - const sessions = sessionsSnapshot.docs.map(doc => { - const data = doc.data(); + let rows: any[]; + if (projectId) { + rows = await query( + `SELECT s.id, s.data, s.created_at + FROM fs_sessions s + JOIN fs_users u ON u.id = s.user_id + WHERE u.data->>'email' = $1 AND s.data->>'projectId' = $2 + ORDER BY s.created_at DESC LIMIT $3`, + [session.user.email, projectId, limit] + ); + } else { + rows = await query( + `SELECT s.id, s.data, s.created_at + FROM fs_sessions s + JOIN fs_users u ON u.id = s.user_id + WHERE u.data->>'email' = $1 + ORDER BY s.created_at DESC LIMIT $2`, + [session.user.email, limit] + ); + } + + const sessions = rows.map((row: any) => { + const d = row.data ?? {}; return { - id: doc.id, - session_id: doc.id, - projectId: data.projectId, - userId: data.userId, - workspacePath: data.workspacePath, - workspaceName: data.workspaceName, - startTime: data.startTime, - endTime: data.endTime, - duration: data.duration, - duration_minutes: data.duration ? Math.round(data.duration / 60) : 0, - tokensUsed: data.tokensUsed || 0, - total_tokens: data.tokensUsed || 0, - cost: data.cost || 0, - estimated_cost_usd: data.cost || 0, - model: data.model || 'unknown', - primary_ai_model: data.model || 'unknown', - filesModified: data.filesModified || [], - summary: data.conversationSummary || null, - message_count: data.messageCount || 0, + id: row.id, + session_id: row.id, + projectId: d.projectId, + userId: d.userId, + workspacePath: d.workspacePath, + workspaceName: d.workspaceName, + startTime: d.startTime, + endTime: d.endTime, + duration: d.duration, + duration_minutes: d.duration ? Math.round(d.duration / 60) : 0, + tokensUsed: d.tokensUsed || 0, + total_tokens: d.tokensUsed || 0, + cost: d.cost || 0, + estimated_cost_usd: d.cost || 0, + model: d.model || 'unknown', + primary_ai_model: d.model || 'unknown', + filesModified: d.filesModified || [], + summary: d.conversationSummary || null, + message_count: d.messageCount || 0, ide_name: 'Cursor', - github_branch: data.githubBranch || null, - conversation: data.conversation || [], - file_changes: data.fileChanges || [], - createdAt: data.createdAt, - last_updated: data.updatedAt || data.createdAt, + github_branch: d.githubBranch || null, + conversation: d.conversation || [], + file_changes: d.fileChanges || [], + createdAt: row.created_at, + last_updated: d.updatedAt || row.created_at, }; }); - console.log(`[API] Found ${sessions.length} sessions from Firebase`); + console.log(`[API] Found ${sessions.length} sessions from PostgreSQL`); return NextResponse.json(sessions); } catch (error) { console.error('[API] Error fetching sessions:', error); diff --git a/app/api/user/api-key/route.ts b/app/api/user/api-key/route.ts index 9ed8ddb..3296818 100644 --- a/app/api/user/api-key/route.ts +++ b/app/api/user/api-key/route.ts @@ -1,76 +1,41 @@ import { NextResponse } from 'next/server'; -import { adminAuth, adminDb } from '@/lib/firebase/admin'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; +import { query } from '@/lib/db-postgres'; import { v4 as uuidv4 } from 'uuid'; -import { FieldValue } from 'firebase-admin/firestore'; export async function GET(request: Request) { try { - console.log('[API] Getting API key...'); - - // Get the authorization header - const authHeader = request.headers.get('authorization'); - if (!authHeader?.startsWith('Bearer ')) { - console.error('[API] No authorization header'); - return NextResponse.json( - { error: 'No authorization token provided' }, - { status: 401 } - ); + const session = await getServerSession(authOptions); + if (!session?.user?.email) { + return NextResponse.json({ error: 'No authorization token provided' }, { status: 401 }); } - const token = authHeader.substring(7); - console.log('[API] Token received, verifying...'); - - // Verify the Firebase ID token - const decodedToken = await adminAuth.verifyIdToken(token); - const userId = decodedToken.uid; - console.log('[API] Token verified, userId:', userId); + const rows = await query<{ data: any }>( + `SELECT data FROM fs_users WHERE data->>'email' = $1 LIMIT 1`, + [session.user.email] + ); - // Check if user already has an API key - console.log('[API] Checking for existing API key...'); - const userDoc = await adminDb.collection('users').doc(userId).get(); - - if (userDoc.exists && userDoc.data()?.apiKey) { - console.log('[API] Found existing API key'); - return NextResponse.json({ - apiKey: userDoc.data()!.apiKey, - }); + if (rows.length > 0 && rows[0].data?.apiKey) { + return NextResponse.json({ apiKey: rows[0].data.apiKey }); } - // Generate new API key - console.log('[API] Generating new API key...'); + // Generate new API key and store it const apiKey = `vibn_${uuidv4().replace(/-/g, '')}`; - - // Store API key document - console.log('[API] Storing API key in Firestore...'); - await adminDb.collection('apiKeys').doc(apiKey).set({ - key: apiKey, - userId, - createdAt: FieldValue.serverTimestamp(), - isActive: true, - }); - - // Update user document with API key reference (or create if doesn't exist) - console.log('[API] Updating user document...'); - await adminDb.collection('users').doc(userId).set({ - apiKey, - updatedAt: FieldValue.serverTimestamp(), - }, { merge: true }); - console.log('[API] API key created successfully:', apiKey); - return NextResponse.json({ - apiKey, - isNew: true, - }); + await query( + `UPDATE fs_users + SET data = data || $1::jsonb, updated_at = NOW() + WHERE data->>'email' = $2`, + [JSON.stringify({ apiKey, apiKeyCreatedAt: new Date().toISOString() }), session.user.email] + ); + + return NextResponse.json({ apiKey, isNew: true }); } catch (error) { console.error('[API] Error getting/creating API key:', error); - console.error('[API] Error stack:', error instanceof Error ? error.stack : 'No stack trace'); return NextResponse.json( - { - error: 'Failed to get API key', - details: error instanceof Error ? error.message : String(error), - }, + { error: 'Failed to get API key', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } - diff --git a/entrypoint.sh b/entrypoint.sh index 359ea07..7f6dc50 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -38,6 +38,19 @@ pool.query(\` ); CREATE INDEX IF NOT EXISTS idx_fs_sessions_user_id ON fs_sessions (user_id); CREATE INDEX IF NOT EXISTS idx_fs_sessions_project_id ON fs_sessions ((data->>'projectId')); + CREATE TABLE IF NOT EXISTS fs_knowledge_items ( + id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, + project_id TEXT NOT NULL, + data JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ); + CREATE INDEX IF NOT EXISTS idx_fs_knowledge_project_id ON fs_knowledge_items (project_id); + CREATE TABLE IF NOT EXISTS chat_conversations ( + project_id TEXT PRIMARY KEY, + messages JSONB NOT NULL DEFAULT '[]', + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); \`).then(() => { console.log('App tables ready'); pool.end(); }).catch(e => { console.error('Table init error:', e.message); pool.end(); }); " diff --git a/lib/server/chat-context.ts b/lib/server/chat-context.ts index c1ce538..b8c186d 100644 --- a/lib/server/chat-context.ts +++ b/lib/server/chat-context.ts @@ -205,17 +205,15 @@ export async function buildProjectContextForChat( }; try { - // Query sessions linked to this project - const sessionsSnapshot = await adminDb - .collection('sessions') - .where('projectId', '==', projectId) - .orderBy('startTime', 'asc') - .get(); + // Query sessions linked to this project from PostgreSQL + const sessionRows = await query<{ id: string; data: any }>( + `SELECT id, data FROM fs_sessions WHERE data->>'projectId' = $1 ORDER BY created_at ASC`, + [projectId] + ); + + if (sessionRows.length > 0) { + sessionHistory.totalSessions = sessionRows.length; - if (!sessionsSnapshot.empty) { - sessionHistory.totalSessions = sessionsSnapshot.size; - - // Extract all messages from all sessions in chronological order const allMessages: Array<{ role: string; content: string; @@ -223,32 +221,27 @@ export async function buildProjectContextForChat( sessionId: string; }> = []; - for (const sessionDoc of sessionsSnapshot.docs) { - const sessionData = sessionDoc.data(); - const conversation = sessionData.conversation || []; - - // Add messages from this session + for (const row of sessionRows) { + const conversation = row.data?.conversation || []; for (const msg of conversation) { if (msg.content && msg.content.trim()) { allMessages.push({ role: msg.role || 'unknown', content: msg.content, - timestamp: msg.timestamp instanceof Date - ? msg.timestamp.toISOString() - : (typeof msg.timestamp === 'string' ? msg.timestamp : new Date().toISOString()), - sessionId: sessionDoc.id, + timestamp: typeof msg.timestamp === 'string' + ? msg.timestamp + : new Date().toISOString(), + sessionId: row.id, }); } } } - // Sort all messages by timestamp (chronological order) - allMessages.sort((a, b) => + allMessages.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); sessionHistory.messages = allMessages; - console.log( `[Chat Context] Loaded ${sessionHistory.totalSessions} sessions with ${allMessages.length} total messages for project ${projectId}` ); diff --git a/lib/server/knowledge.ts b/lib/server/knowledge.ts index 4b5fb70..1fa6151 100644 --- a/lib/server/knowledge.ts +++ b/lib/server/knowledge.ts @@ -1,13 +1,11 @@ -import { getAdminDb } from '@/lib/firebase/admin'; -import { FieldValue } from 'firebase-admin/firestore'; +import { query } from '@/lib/db-postgres'; +import { randomUUID } from 'crypto'; import type { KnowledgeItem, KnowledgeSourceMeta, KnowledgeSourceType, } from '@/lib/types/knowledge'; -const COLLECTION = 'knowledge_items'; - interface CreateKnowledgeItemInput { projectId: string; sourceType: KnowledgeSourceType; @@ -16,59 +14,62 @@ interface CreateKnowledgeItemInput { sourceMeta?: KnowledgeSourceMeta; } +function rowToItem(row: { id: string; project_id: string; data: any; created_at: string; updated_at: string }): KnowledgeItem { + const d = row.data ?? {}; + return { + id: row.id, + projectId: row.project_id, + sourceType: d.sourceType, + title: d.title ?? null, + content: d.content, + sourceMeta: d.sourceMeta ?? null, + createdAt: row.created_at, + updatedAt: row.updated_at, + } as KnowledgeItem; +} + export async function createKnowledgeItem( input: CreateKnowledgeItemInput, ): Promise { - const adminDb = getAdminDb(); - const docRef = adminDb.collection(COLLECTION).doc(); - - const payload = { - id: docRef.id, - projectId: input.projectId, + const id = randomUUID(); + const data = { sourceType: input.sourceType, title: input.title ?? null, content: input.content, sourceMeta: input.sourceMeta ?? null, - createdAt: FieldValue.serverTimestamp(), - updatedAt: FieldValue.serverTimestamp(), }; - await docRef.set(payload); - const snapshot = await docRef.get(); - return snapshot.data() as KnowledgeItem; + await query( + `INSERT INTO fs_knowledge_items (id, project_id, data) VALUES ($1, $2, $3::jsonb)`, + [id, input.projectId, JSON.stringify(data)] + ); + + const rows = await query( + `SELECT id, project_id, data, created_at, updated_at FROM fs_knowledge_items WHERE id = $1`, + [id] + ); + + return rowToItem(rows[0]); } export async function getKnowledgeItem( projectId: string, knowledgeItemId: string, ): Promise { - const adminDb = getAdminDb(); - const docRef = adminDb.collection(COLLECTION).doc(knowledgeItemId); - const snapshot = await docRef.get(); - - if (!snapshot.exists) { - return null; - } - - const data = snapshot.data() as KnowledgeItem; - if (data.projectId !== projectId) { - return null; - } - - return data; + const rows = await query( + `SELECT id, project_id, data, created_at, updated_at FROM fs_knowledge_items WHERE id = $1 AND project_id = $2`, + [knowledgeItemId, projectId] + ); + if (rows.length === 0) return null; + return rowToItem(rows[0]); } export async function listKnowledgeItems( projectId: string, ): Promise { - const adminDb = getAdminDb(); - const querySnapshot = await adminDb - .collection(COLLECTION) - .where('projectId', '==', projectId) - .orderBy('createdAt', 'desc') - .get(); - - return querySnapshot.docs.map((doc) => doc.data() as KnowledgeItem); + const rows = await query( + `SELECT id, project_id, data, created_at, updated_at FROM fs_knowledge_items WHERE project_id = $1 ORDER BY created_at DESC`, + [projectId] + ); + return rows.map(rowToItem); } - -