import { and, desc, eq } from 'drizzle-orm'; import type { ResearchJournalEntry, ResearchJournalEntryType } from '@/lib/types'; import { db } from '@/lib/server/db'; import { researchJournalEntry } from '@/lib/server/db/schema'; type ResearchJournalRow = typeof researchJournalEntry.$inferSelect; function normalizeTicker(ticker: string) { return ticker.trim().toUpperCase(); } function normalizeTitle(title?: string | null) { const normalized = title?.trim(); return normalized ? normalized : null; } function normalizeAccessionNumber(accessionNumber?: string | null) { const normalized = accessionNumber?.trim(); return normalized ? normalized : null; } function normalizeMetadata(metadata?: Record | null) { if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) { return null; } return metadata; } function toResearchJournalEntry(row: ResearchJournalRow): ResearchJournalEntry { return { id: row.id, user_id: row.user_id, ticker: row.ticker, accession_number: row.accession_number ?? null, entry_type: row.entry_type, title: row.title ?? null, body_markdown: row.body_markdown, metadata: row.metadata ?? null, created_at: row.created_at, updated_at: row.updated_at }; } export async function listResearchJournalEntries(userId: string, ticker: string, limit = 100) { const normalizedTicker = normalizeTicker(ticker); if (!normalizedTicker) { return []; } const safeLimit = Math.min(Math.max(Math.trunc(limit), 1), 250); const rows = await db .select() .from(researchJournalEntry) .where(and(eq(researchJournalEntry.user_id, userId), eq(researchJournalEntry.ticker, normalizedTicker))) .orderBy(desc(researchJournalEntry.created_at), desc(researchJournalEntry.id)) .limit(safeLimit); return rows.map(toResearchJournalEntry); } export async function listResearchJournalEntriesForUser(userId: string, limit = 250) { const safeLimit = Math.min(Math.max(Math.trunc(limit), 1), 500); const rows = await db .select() .from(researchJournalEntry) .where(eq(researchJournalEntry.user_id, userId)) .orderBy(desc(researchJournalEntry.updated_at), desc(researchJournalEntry.id)) .limit(safeLimit); return rows.map(toResearchJournalEntry); } export async function getResearchJournalEntryRecord(userId: string, id: number) { const [row] = await db .select() .from(researchJournalEntry) .where(and(eq(researchJournalEntry.user_id, userId), eq(researchJournalEntry.id, id))) .limit(1); return row ? toResearchJournalEntry(row) : null; } export async function createResearchJournalEntryRecord(input: { userId: string; ticker: string; accessionNumber?: string | null; entryType: ResearchJournalEntryType; title?: string | null; bodyMarkdown: string; metadata?: Record | null; }) { const ticker = normalizeTicker(input.ticker); const bodyMarkdown = input.bodyMarkdown.trim(); if (!ticker) { throw new Error('ticker is required'); } if (!bodyMarkdown) { throw new Error('bodyMarkdown is required'); } const now = new Date().toISOString(); const [created] = await db .insert(researchJournalEntry) .values({ user_id: input.userId, ticker, accession_number: normalizeAccessionNumber(input.accessionNumber), entry_type: input.entryType, title: normalizeTitle(input.title), body_markdown: bodyMarkdown, metadata: normalizeMetadata(input.metadata), created_at: now, updated_at: now }) .returning(); return toResearchJournalEntry(created); } export async function updateResearchJournalEntryRecord(input: { userId: string; id: number; title?: string | null; bodyMarkdown?: string; metadata?: Record | null; }) { const [existing] = await db .select() .from(researchJournalEntry) .where(and(eq(researchJournalEntry.user_id, input.userId), eq(researchJournalEntry.id, input.id))) .limit(1); if (!existing) { return null; } const nextBodyMarkdown = input.bodyMarkdown === undefined ? existing.body_markdown : input.bodyMarkdown.trim(); if (!nextBodyMarkdown) { throw new Error('bodyMarkdown is required'); } const [updated] = await db .update(researchJournalEntry) .set({ title: input.title === undefined ? existing.title : normalizeTitle(input.title), body_markdown: nextBodyMarkdown, metadata: input.metadata === undefined ? existing.metadata ?? null : normalizeMetadata(input.metadata), updated_at: new Date().toISOString() }) .where(and(eq(researchJournalEntry.user_id, input.userId), eq(researchJournalEntry.id, input.id))) .returning(); return updated ? toResearchJournalEntry(updated) : null; } export async function deleteResearchJournalEntryRecord(userId: string, id: number) { const rows = await db .delete(researchJournalEntry) .where(and(eq(researchJournalEntry.user_id, userId), eq(researchJournalEntry.id, id))) .returning({ id: researchJournalEntry.id }); return rows.length > 0; }