141 lines
3.4 KiB
TypeScript
141 lines
3.4 KiB
TypeScript
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<string>();
|
|
|
|
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;
|
|
}
|