Add research workspace and graphing flows
This commit is contained in:
@@ -1,148 +1,6 @@
|
||||
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;
|
||||
}
|
||||
export {
|
||||
createResearchJournalEntryCompat as createResearchJournalEntryRecord,
|
||||
deleteResearchJournalEntryCompat as deleteResearchJournalEntryRecord,
|
||||
listResearchJournalEntriesCompat as listResearchJournalEntries,
|
||||
updateResearchJournalEntryCompat as updateResearchJournalEntryRecord
|
||||
} from '@/lib/server/repos/research-library';
|
||||
|
||||
Reference in New Issue
Block a user