190 lines
5.5 KiB
TypeScript
190 lines
5.5 KiB
TypeScript
import { edenTreaty } from '@elysiajs/eden';
|
|
import type { App } from '@/lib/server/api/app';
|
|
import type {
|
|
Filing,
|
|
Holding,
|
|
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 result = await client.api.filings.get({
|
|
$query: {
|
|
ticker: query?.ticker,
|
|
limit: query?.limit
|
|
}
|
|
});
|
|
|
|
return await unwrapData<{ filings: Filing[] }>(result, 'Unable to fetch filings');
|
|
}
|
|
|
|
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');
|
|
}
|