import { eq, desc, asc, sql } from "drizzle-orm"; import { drizzle } from "drizzle-orm/mysql2"; import { InsertUser, User, users, projects, iterations, codeReferences, statistics } from "../drizzle/schema"; import { ENV } from './_core/env'; let _db: ReturnType | null = null; // In-memory storage for fallback mode const memoryStore = { users: [] as User[], projects: [] as any[], iterations: [] as any[], codeReferences: [] as any[], statistics: [] as any[], }; // Lazily create the drizzle instance so local tooling can run without a DB. export async function getDb() { if (!_db && process.env.DATABASE_URL) { try { _db = drizzle(process.env.DATABASE_URL); } catch (error) { console.warn("[Database] Failed to connect, using memory fallback:", error); _db = null; } } return _db; } export async function upsertUser(user: InsertUser): Promise { if (!user.openId) { throw new Error("User openId is required for upsert"); } const db = await getDb(); if (!db) { console.warn("[Database] Using memory fallback for upsertUser"); const existingIdx = memoryStore.users.findIndex(u => u.openId === user.openId); const userData: User = { id: existingIdx >= 0 ? memoryStore.users[existingIdx].id : memoryStore.users.length + 1, openId: user.openId, name: user.name ?? null, email: user.email ?? null, loginMethod: user.loginMethod ?? null, role: user.role ?? (user.openId === ENV.ownerOpenId ? 'admin' : 'user'), createdAt: existingIdx >= 0 ? memoryStore.users[existingIdx].createdAt : new Date(), updatedAt: new Date(), lastSignedIn: user.lastSignedIn ?? new Date(), }; if (existingIdx >= 0) { memoryStore.users[existingIdx] = userData; } else { memoryStore.users.push(userData); } return; } try { const values: InsertUser = { openId: user.openId, }; const updateSet: Record = {}; const textFields = ["name", "email", "loginMethod"] as const; type TextField = (typeof textFields)[number]; const assignNullable = (field: TextField) => { const value = user[field]; if (value === undefined) return; const normalized = value ?? null; values[field] = normalized; updateSet[field] = normalized; }; textFields.forEach(assignNullable); if (user.lastSignedIn !== undefined) { values.lastSignedIn = user.lastSignedIn; updateSet.lastSignedIn = user.lastSignedIn; } if (user.role !== undefined) { values.role = user.role; updateSet.role = user.role; } else if (user.openId === ENV.ownerOpenId) { values.role = 'admin'; updateSet.role = 'admin'; } if (!values.lastSignedIn) { values.lastSignedIn = new Date(); } if (Object.keys(updateSet).length === 0) { updateSet.lastSignedIn = new Date(); } await db.insert(users).values(values).onDuplicateKeyUpdate({ set: updateSet, }); } catch (error) { console.error("[Database] Failed to upsert user:", error); throw error; } } export async function getUserByOpenId(openId: string) { const db = await getDb(); if (!db) { return memoryStore.users.find(u => u.openId === openId); } const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function getOrCreateGuestUser() { const guestOpenId = "guest-user"; let user = await getUserByOpenId(guestOpenId); if (!user) { await upsertUser({ openId: guestOpenId, name: "Guest User", email: "guest@example.com", role: "admin", }); user = await getUserByOpenId(guestOpenId); } return user; } // Projects queries export async function createProject(userId: number, data: { name: string; description?: string; mode: string; contentType: string; originalPrompt: string; }) { const db = await getDb(); if (!db) { const newProject = { id: memoryStore.projects.length + 1, userId, name: data.name, description: data.description ?? null, mode: data.mode, contentType: data.contentType, originalPrompt: data.originalPrompt, status: "pending", createdAt: new Date(), updatedAt: new Date(), finalOutput: null, finalScore: 0, completedAt: null, }; memoryStore.projects.push(newProject); return newProject; } const result = await db.insert(projects).values({ userId, name: data.name, description: data.description, mode: data.mode as any, contentType: data.contentType as any, originalPrompt: data.originalPrompt, status: "pending", }); const projectId = (result as any).insertId || 0; if (projectId > 0) { const newProject = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1); return newProject[0] || null; } return null; } export async function getUserProjects(userId: number, limit = 50) { const db = await getDb(); if (!db) { return memoryStore.projects .filter(p => p.userId === userId) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) .slice(0, limit); } return await db.select().from(projects) .where(eq(projects.userId, userId)) .orderBy(desc(projects.createdAt)) .limit(limit); } export async function getProjectById(projectId: number) { const db = await getDb(); if (!db) { return memoryStore.projects.find(p => p.id === projectId) || null; } const result = await db.select().from(projects) .where(eq(projects.id, projectId)) .limit(1); return result[0] || null; } export async function updateProjectStatus(projectId: number, status: string, finalOutput?: string, finalScore?: number) { const db = await getDb(); if (!db) { const project = memoryStore.projects.find(p => p.id === projectId); if (project) { project.status = status; if (finalOutput !== undefined) project.finalOutput = finalOutput; if (finalScore !== undefined) project.finalScore = finalScore; project.updatedAt = new Date(); if (status === "completed") project.completedAt = new Date(); return true; } return false; } await db.update(projects) .set({ status: status as any, finalOutput, finalScore, completedAt: status === "completed" ? new Date() : undefined, }) .where(eq(projects.id, projectId)); return true; } // Iterations queries export async function addIteration(projectId: number, version: number, data: { qwenOutput?: string; deepseekAnalysis?: string; score: number; passed: boolean; scorecard?: any; feedback?: any; }) { const db = await getDb(); if (!db) { const newIteration = { id: memoryStore.iterations.length + 1, projectId, version, qwenOutput: data.qwenOutput ?? null, deepseekAnalysis: data.deepseekAnalysis ?? null, score: data.score, passed: data.passed ? 1 : 0, scorecard: data.scorecard ? JSON.stringify(data.scorecard) : null, feedback: data.feedback ? JSON.stringify(data.feedback) : null, createdAt: new Date(), }; memoryStore.iterations.push(newIteration); return { insertId: newIteration.id }; } const result = await db.insert(iterations).values({ projectId, version, qwenOutput: data.qwenOutput, deepseekAnalysis: data.deepseekAnalysis, score: data.score, passed: data.passed ? 1 : 0, scorecard: data.scorecard ? JSON.stringify(data.scorecard) : null, feedback: data.feedback ? JSON.stringify(data.feedback) : null, }); return result; } export async function getProjectIterations(projectId: number) { const db = await getDb(); if (!db) { return memoryStore.iterations .filter(i => i.projectId === projectId) .sort((a, b) => a.version - b.version); } return await db.select().from(iterations) .where(eq(iterations.projectId, projectId)) .orderBy(asc(iterations.version)); } // Code References queries export async function uploadCodeReference(userId: number, data: { filename: string; category: string; description?: string; content?: string; s3Key?: string; s3Url?: string; mimeType?: string; fileSize?: number; analysis?: any; tags?: string[]; }) { const db = await getDb(); if (!db) { const newRef = { id: memoryStore.codeReferences.length + 1, userId, filename: data.filename, category: data.category, description: data.description ?? null, content: data.content ?? null, s3Key: data.s3Key ?? null, s3Url: data.s3Url ?? null, mimeType: data.mimeType ?? null, fileSize: data.fileSize ?? null, analysis: data.analysis ? JSON.stringify(data.analysis) : null, tags: data.tags ? JSON.stringify(data.tags) : null, createdAt: new Date(), updatedAt: new Date(), }; memoryStore.codeReferences.push(newRef); return { insertId: newRef.id }; } const result = await db.insert(codeReferences).values({ userId, filename: data.filename, category: data.category as any, description: data.description, content: data.content, s3Key: data.s3Key, s3Url: data.s3Url, mimeType: data.mimeType, fileSize: data.fileSize, analysis: data.analysis ? JSON.stringify(data.analysis) : null, tags: data.tags ? JSON.stringify(data.tags) : null, }); return result; } export async function getUserCodeReferences(userId: number, category?: string) { const db = await getDb(); if (!db) { return memoryStore.codeReferences .filter(r => r.userId === userId && (!category || r.category === category)) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); } return await db.select().from(codeReferences) .where(eq(codeReferences.userId, userId)) .orderBy(desc(codeReferences.createdAt)); } export async function searchCodeReferences(userId: number, searchTerm: string) { const db = await getDb(); if (!db) { return memoryStore.codeReferences .filter(r => r.userId === userId && (r.filename.includes(searchTerm) || (r.description && r.description.includes(searchTerm)))) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); } return await db.select().from(codeReferences) .where(eq(codeReferences.userId, userId)) .orderBy(desc(codeReferences.createdAt)); } // Statistics queries export async function updateDailyStatistics(date: string) { const db = await getDb(); if (!db) return true; const projectsData = await db.select({ total: sql`COUNT(*) as total`, successful: sql`SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as successful`, failed: sql`SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed`, avgScore: sql`AVG(finalScore) as avgScore`, }).from(projects) .where(sql`DATE(createdAt) = ${date}`); const data = projectsData[0] || {}; await db.insert(statistics).values({ date, totalProjects: (data.total as number) || 0, successfulProjects: (data.successful as number) || 0, failedProjects: (data.failed as number) || 0, avgScore: Math.round((data.avgScore as number) || 0), }).onDuplicateKeyUpdate({ set: { totalProjects: (data.total as number) || 0, successfulProjects: (data.successful as number) || 0, failedProjects: (data.failed as number) || 0, avgScore: Math.round((data.avgScore as number) || 0), }, }); return true; }