149 lines
4.3 KiB
TypeScript
149 lines
4.3 KiB
TypeScript
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<string, unknown> | 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 createResearchJournalEntryRecord(input: {
|
|
userId: string;
|
|
ticker: string;
|
|
accessionNumber?: string | null;
|
|
entryType: ResearchJournalEntryType;
|
|
title?: string | null;
|
|
bodyMarkdown: string;
|
|
metadata?: Record<string, unknown> | 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<string, unknown> | 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;
|
|
}
|