import { and, desc, eq } from 'drizzle-orm'; import type { WatchlistItem } from '@/lib/types'; import { db } from '@/lib/server/db'; import { watchlistItem } from '@/lib/server/db/schema'; type WatchlistRow = typeof watchlistItem.$inferSelect; function normalizeTags(tags?: string[]) { if (!Array.isArray(tags)) { return null; } const unique = new Set(); for (const entry of tags) { if (typeof entry !== 'string') { continue; } const tag = entry.trim(); if (!tag) { continue; } unique.add(tag); } if (unique.size === 0) { return null; } return [...unique]; } function toWatchlistItem(row: WatchlistRow): WatchlistItem { return { id: row.id, user_id: row.user_id, ticker: row.ticker, company_name: row.company_name, sector: row.sector, category: row.category, tags: Array.isArray(row.tags) ? row.tags.filter((entry): entry is string => typeof entry === 'string') : [], created_at: row.created_at }; } export async function listWatchlistItems(userId: string) { const rows = await db .select() .from(watchlistItem) .where(eq(watchlistItem.user_id, userId)) .orderBy(desc(watchlistItem.created_at)); return rows.map(toWatchlistItem); } export async function getWatchlistItemByTicker(userId: string, ticker: string) { const normalizedTicker = ticker.trim().toUpperCase(); if (!normalizedTicker) { return null; } const [row] = await db .select() .from(watchlistItem) .where(and(eq(watchlistItem.user_id, userId), eq(watchlistItem.ticker, normalizedTicker))) .limit(1); return row ? toWatchlistItem(row) : null; } export async function upsertWatchlistItemRecord(input: { userId: string; ticker: string; companyName: string; sector?: string; category?: string; tags?: string[]; }) { const normalizedTicker = input.ticker.trim().toUpperCase(); const normalizedSector = input.sector?.trim() ? input.sector.trim() : null; const normalizedCategory = input.category?.trim() ? input.category.trim() : null; const normalizedTags = normalizeTags(input.tags); const now = new Date().toISOString(); const [inserted] = await db .insert(watchlistItem) .values({ user_id: input.userId, ticker: normalizedTicker, company_name: input.companyName, sector: normalizedSector, category: normalizedCategory, tags: normalizedTags, created_at: now }) .onConflictDoNothing({ target: [watchlistItem.user_id, watchlistItem.ticker], }) .returning(); if (inserted) { return { item: toWatchlistItem(inserted), created: true }; } const [updated] = await db .update(watchlistItem) .set({ company_name: input.companyName, sector: normalizedSector, category: normalizedCategory, tags: normalizedTags }) .where(and(eq(watchlistItem.user_id, input.userId), eq(watchlistItem.ticker, normalizedTicker))) .returning(); if (!updated) { throw new Error(`Watchlist item ${normalizedTicker} was not found after upsert conflict resolution`); } return { item: toWatchlistItem(updated), created: false }; } export async function deleteWatchlistItemRecord(userId: string, id: number) { const removed = await db .delete(watchlistItem) .where(and(eq(watchlistItem.user_id, userId), eq(watchlistItem.id, id))) .returning({ id: watchlistItem.id }); return removed.length > 0; }