Files
Neon-Desk/lib/api.ts

256 lines
7.4 KiB
TypeScript

import { edenTreaty } from '@elysiajs/eden';
import type { App } from '@/lib/server/api/app';
import type {
CompanyAiReportDetail,
CompanyAnalysis,
CompanyFinancialStatementsResponse,
Filing,
Holding,
FinancialHistoryWindow,
FinancialStatementKind,
FinancialStatementMode,
PortfolioInsight,
PortfolioSummary,
Task,
User,
WatchlistItem
} from './types';
import { resolveApiBaseURL } from './runtime-url';
const API_BASE = resolveApiBaseURL(process.env.NEXT_PUBLIC_API_URL);
const client = edenTreaty<App>(API_BASE, {
$fetch: {
credentials: 'include',
cache: 'no-store'
}
});
export class ApiError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.name = 'ApiError';
this.status = status;
}
}
function extractErrorMessage(error: unknown, fallback: string) {
if (!error || typeof error !== 'object') {
return fallback;
}
const candidate = error as {
value?: unknown;
message?: string;
};
if (typeof candidate.message === 'string' && candidate.message.trim().length > 0) {
return candidate.message;
}
if (candidate.value && typeof candidate.value === 'object') {
const nested = candidate.value as { error?: unknown; message?: unknown };
if (typeof nested.error === 'string' && nested.error.trim().length > 0) {
return nested.error;
}
if (typeof nested.message === 'string' && nested.message.trim().length > 0) {
return nested.message;
}
}
if (typeof candidate.value === 'string' && candidate.value.trim().length > 0) {
return candidate.value;
}
return fallback;
}
type TreatyResult = {
data: unknown;
error: unknown;
status: number;
};
async function unwrapData<T>(result: TreatyResult, fallback: string) {
if (result.error) {
throw new ApiError(
extractErrorMessage(result.error, fallback),
result.status
);
}
if (result.data === null || result.data === undefined) {
throw new ApiError(fallback, result.status);
}
const payload = result.data instanceof Response
? await result.data.json().catch(() => null)
: result.data;
if (payload === null || payload === undefined) {
throw new ApiError(fallback, result.status);
}
return payload as T;
}
export async function getMe() {
const result = await client.api.me.get();
return await unwrapData<{ user: User }>(result, 'Unable to fetch session');
}
export async function listWatchlist() {
const result = await client.api.watchlist.get();
return await unwrapData<{ items: WatchlistItem[] }>(result, 'Unable to fetch watchlist');
}
export async function upsertWatchlistItem(input: { ticker: string; companyName: string; sector?: string }) {
const result = await client.api.watchlist.post(input);
return await unwrapData<{ item: WatchlistItem }>(result, 'Unable to save watchlist item');
}
export async function deleteWatchlistItem(id: number) {
const result = await client.api.watchlist[id].delete();
return await unwrapData<{ success: boolean }>(result, 'Unable to delete watchlist item');
}
export async function listHoldings() {
const result = await client.api.portfolio.holdings.get();
return await unwrapData<{ holdings: Holding[] }>(result, 'Unable to fetch holdings');
}
export async function getPortfolioSummary() {
const result = await client.api.portfolio.summary.get();
return await unwrapData<{ summary: PortfolioSummary }>(result, 'Unable to fetch summary');
}
export async function upsertHolding(input: {
ticker: string;
shares: number;
avgCost: number;
currentPrice?: number;
}) {
const result = await client.api.portfolio.holdings.post(input);
return await unwrapData<{ holding: Holding }>(result, 'Unable to save holding');
}
export async function deleteHolding(id: number) {
const result = await client.api.portfolio.holdings[id].delete();
return await unwrapData<{ success: boolean }>(result, 'Unable to delete holding');
}
export async function queuePriceRefresh() {
const result = await client.api.portfolio['refresh-prices'].post();
return await unwrapData<{ task: Task }>(result, 'Unable to queue price refresh');
}
export async function queuePortfolioInsights() {
const result = await client.api.portfolio.insights.generate.post();
return await unwrapData<{ task: Task }>(result, 'Unable to queue portfolio insights');
}
export async function getLatestPortfolioInsight() {
const result = await client.api.portfolio.insights.latest.get();
return await unwrapData<{ insight: PortfolioInsight | null }>(result, 'Unable to fetch latest insight');
}
export async function listFilings(query?: { ticker?: string; limit?: number }) {
const queryParams: {
ticker?: string;
limit?: number;
} = {};
if (query?.ticker?.trim()) {
queryParams.ticker = query.ticker.trim().toUpperCase();
}
if (query?.limit !== undefined) {
queryParams.limit = query.limit;
}
const result = await client.api.filings.get({
$query: queryParams
});
return await unwrapData<{ filings: Filing[] }>(result, 'Unable to fetch filings');
}
export async function getCompanyAnalysis(ticker: string) {
const result = await client.api.analysis.company.get({
$query: {
ticker: ticker.trim().toUpperCase()
}
});
return await unwrapData<{ analysis: CompanyAnalysis }>(result, 'Unable to fetch company analysis');
}
export async function getCompanyFinancialStatements(input: {
ticker: string;
mode: FinancialStatementMode;
statement: FinancialStatementKind;
window: FinancialHistoryWindow;
includeDimensions?: boolean;
cursor?: string | null;
limit?: number;
}) {
const query = {
ticker: input.ticker.trim().toUpperCase(),
mode: input.mode,
statement: input.statement,
window: input.window,
includeDimensions: input.includeDimensions ? 'true' : 'false',
...(typeof input.cursor === 'string' && input.cursor.trim().length > 0
? { cursor: input.cursor.trim() }
: {}),
...(typeof input.limit === 'number' && Number.isFinite(input.limit)
? { limit: input.limit }
: {})
};
const result = await client.api.financials.company.get({
$query: query
});
return await unwrapData<{ financials: CompanyFinancialStatementsResponse }>(
result,
'Unable to fetch company financial statements'
);
}
export async function getCompanyAiReport(accessionNumber: string) {
const normalizedAccession = accessionNumber.trim();
const result = await client.api.analysis.reports[normalizedAccession].get();
return await unwrapData<{ report: CompanyAiReportDetail }>(result, 'Unable to fetch AI summary');
}
export async function queueFilingSync(input: { ticker: string; limit?: number }) {
const result = await client.api.filings.sync.post(input);
return await unwrapData<{ task: Task }>(result, 'Unable to queue filing sync');
}
export async function queueFilingAnalysis(accessionNumber: string) {
const result = await client.api.filings[accessionNumber].analyze.post();
return await unwrapData<{ task: Task }>(result, 'Unable to queue filing analysis');
}
export async function getTask(taskId: string) {
const result = await client.api.tasks[taskId].get();
return await unwrapData<{ task: Task }>(result, 'Unable to fetch task');
}
export async function listRecentTasks(limit = 20) {
const result = await client.api.tasks.get({
$query: {
limit
}
});
return await unwrapData<{ tasks: Task[] }>(result, 'Unable to fetch tasks');
}