Consolidate server utilities into shared module

- Add lib/server/utils/normalize.ts with normalizeTicker, normalizeTagsOrNull, nowIso, todayIso
- Add lib/server/utils/validation.ts with asRecord, asBoolean, asStringArray, asEnum
- Add lib/server/utils/index.ts re-exporting all utilities
- Remove duplicate lib/server/utils.ts (old file)
- Update all repos and files to use shared utilities
- Remove redundant ?? '' from normalizeTicker calls
- Update watchlist.ts to use normalizeTagsOrNull for null-return tags
This commit is contained in:
2026-03-15 15:56:16 -04:00
parent edf1cfb421
commit 5f0abbb007
14 changed files with 193 additions and 127 deletions

View File

@@ -27,6 +27,14 @@ import {
} from '@/lib/server/db/schema';
import { getFilingByAccession, listFilingsRecords } from '@/lib/server/repos/filings';
import { getWatchlistItemByTicker } from '@/lib/server/repos/watchlist';
import {
normalizeTicker,
normalizeTags,
normalizeOptionalString,
normalizeRecord,
normalizePositiveInteger,
nowIso
} from '@/lib/server/utils';
type ResearchArtifactRow = typeof researchArtifact.$inferSelect;
type ResearchMemoRow = typeof researchMemo.$inferSelect;
@@ -51,55 +59,6 @@ const RESEARCH_PACKET_SECTION_TITLES: Record<ResearchMemoSection, string> = {
next_actions: 'Next Actions'
};
function normalizeTicker(ticker: string) {
return ticker.trim().toUpperCase();
}
function normalizeOptionalString(value?: string | null) {
const normalized = value?.trim();
return normalized ? normalized : null;
}
function normalizePositiveInteger(value?: number | null) {
if (value === null || value === undefined || !Number.isFinite(value)) {
return null;
}
const normalized = Math.trunc(value);
return normalized > 0 ? normalized : null;
}
function normalizeRecord(value?: Record<string, unknown> | null) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value;
}
function normalizeTags(tags?: string[] | null) {
if (!Array.isArray(tags)) {
return [];
}
const unique = new Set<string>();
for (const entry of tags) {
if (typeof entry !== 'string') {
continue;
}
const normalized = entry.trim();
if (!normalized) {
continue;
}
unique.add(normalized);
}
return [...unique];
}
function buildArtifactSearchText(input: {
title?: string | null;
summary?: string | null;
@@ -409,7 +368,7 @@ export async function createResearchArtifactRecord(input: {
throw new Error('ticker is required');
}
const now = new Date().toISOString();
const now = nowIso();
const title = normalizeOptionalString(input.title);
const summary = normalizeOptionalString(input.summary);
const bodyMarkdown = normalizeOptionalString(input.bodyMarkdown);
@@ -520,7 +479,7 @@ export async function upsertSystemResearchArtifact(input: {
search_text: searchText,
tags: normalizeTags(input.tags),
metadata: normalizeRecord(input.metadata),
updated_at: new Date().toISOString()
updated_at: nowIso()
})
.where(eq(researchArtifact.id, existing.id))
.returning();
@@ -566,7 +525,7 @@ export async function updateResearchArtifactRecord(input: {
metadata,
tags,
search_text: searchText,
updated_at: new Date().toISOString()
updated_at: nowIso()
})
.where(eq(researchArtifact.id, input.id))
.returning();
@@ -702,7 +661,7 @@ export async function upsertResearchMemoRecord(input: {
throw new Error('ticker is required');
}
const now = new Date().toISOString();
const now = nowIso();
const existing = await getResearchMemoByTicker(input.userId, ticker);
if (!existing) {
@@ -796,7 +755,7 @@ export async function addResearchMemoEvidenceLink(input: {
.then((rows) => (rows[0]?.maxOrder ?? 0) + 1)
: Math.max(0, Math.trunc(input.sortOrder));
const now = new Date().toISOString();
const now = nowIso();
if (existing.length > 0) {
await db
.update(researchMemoEvidence)
@@ -894,7 +853,7 @@ export async function getResearchPacket(userId: string, ticker: string): Promise
return {
ticker: normalizedTicker,
companyName: coverage?.company_name ?? latestFiling?.company_name ?? null,
generated_at: new Date().toISOString(),
generated_at: nowIso(),
memo,
sections: toPacketSections(memo, evidenceBySection)
};