chore: commit all changes
This commit is contained in:
167
lib/api.ts
167
lib/api.ts
@@ -1,3 +1,5 @@
|
||||
import { edenTreaty } from '@elysiajs/eden';
|
||||
import type { App } from '@/lib/server/api/app';
|
||||
import type {
|
||||
Filing,
|
||||
Holding,
|
||||
@@ -11,6 +13,13 @@ 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;
|
||||
|
||||
@@ -21,56 +30,96 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const headers = new Headers(init?.headers);
|
||||
if (!headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
function extractErrorMessage(error: unknown, fallback: string) {
|
||||
if (!error || typeof error !== 'object') {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
...init,
|
||||
credentials: 'include',
|
||||
headers,
|
||||
cache: 'no-store'
|
||||
});
|
||||
const candidate = error as {
|
||||
value?: unknown;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const body = await response.json().catch(() => ({}));
|
||||
|
||||
if (!response.ok) {
|
||||
const message = typeof body?.error === 'string' ? body.error : `Request failed (${response.status})`;
|
||||
throw new ApiError(message, response.status);
|
||||
if (typeof candidate.message === 'string' && candidate.message.trim().length > 0) {
|
||||
return candidate.message;
|
||||
}
|
||||
|
||||
return body as T;
|
||||
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() {
|
||||
return await apiFetch<{ user: User }>('/api/me');
|
||||
const result = await client.api.me.get();
|
||||
return await unwrapData<{ user: User }>(result, 'Unable to fetch session');
|
||||
}
|
||||
|
||||
export async function listWatchlist() {
|
||||
return await apiFetch<{ items: WatchlistItem[] }>('/api/watchlist');
|
||||
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 }) {
|
||||
return await apiFetch<{ item: WatchlistItem }>('/api/watchlist', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input)
|
||||
});
|
||||
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) {
|
||||
return await apiFetch<{ success: boolean }>(`/api/watchlist/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await client.api.watchlist[id].delete();
|
||||
return await unwrapData<{ success: boolean }>(result, 'Unable to delete watchlist item');
|
||||
}
|
||||
|
||||
export async function listHoldings() {
|
||||
return await apiFetch<{ holdings: Holding[] }>('/api/portfolio/holdings');
|
||||
const result = await client.api.portfolio.holdings.get();
|
||||
return await unwrapData<{ holdings: Holding[] }>(result, 'Unable to fetch holdings');
|
||||
}
|
||||
|
||||
export async function getPortfolioSummary() {
|
||||
return await apiFetch<{ summary: PortfolioSummary }>('/api/portfolio/summary');
|
||||
const result = await client.api.portfolio.summary.get();
|
||||
return await unwrapData<{ summary: PortfolioSummary }>(result, 'Unable to fetch summary');
|
||||
}
|
||||
|
||||
export async function upsertHolding(input: {
|
||||
@@ -79,66 +128,62 @@ export async function upsertHolding(input: {
|
||||
avgCost: number;
|
||||
currentPrice?: number;
|
||||
}) {
|
||||
return await apiFetch<{ holding: Holding }>('/api/portfolio/holdings', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input)
|
||||
});
|
||||
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) {
|
||||
return await apiFetch<{ success: boolean }>(`/api/portfolio/holdings/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await client.api.portfolio.holdings[id].delete();
|
||||
return await unwrapData<{ success: boolean }>(result, 'Unable to delete holding');
|
||||
}
|
||||
|
||||
export async function queuePriceRefresh() {
|
||||
return await apiFetch<{ task: Task }>('/api/portfolio/refresh-prices', {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await client.api.portfolio['refresh-prices'].post();
|
||||
return await unwrapData<{ task: Task }>(result, 'Unable to queue price refresh');
|
||||
}
|
||||
|
||||
export async function queuePortfolioInsights() {
|
||||
return await apiFetch<{ task: Task }>('/api/portfolio/insights/generate', {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await client.api.portfolio.insights.generate.post();
|
||||
return await unwrapData<{ task: Task }>(result, 'Unable to queue portfolio insights');
|
||||
}
|
||||
|
||||
export async function getLatestPortfolioInsight() {
|
||||
return await apiFetch<{ insight: PortfolioInsight | null }>('/api/portfolio/insights/latest');
|
||||
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 params = new URLSearchParams();
|
||||
const result = await client.api.filings.get({
|
||||
$query: {
|
||||
ticker: query?.ticker,
|
||||
limit: query?.limit
|
||||
}
|
||||
});
|
||||
|
||||
if (query?.ticker) {
|
||||
params.set('ticker', query.ticker);
|
||||
}
|
||||
|
||||
if (query?.limit) {
|
||||
params.set('limit', String(query.limit));
|
||||
}
|
||||
|
||||
const suffix = params.size > 0 ? `?${params.toString()}` : '';
|
||||
return await apiFetch<{ filings: Filing[] }>(`/api/filings${suffix}`);
|
||||
return await unwrapData<{ filings: Filing[] }>(result, 'Unable to fetch filings');
|
||||
}
|
||||
|
||||
export async function queueFilingSync(input: { ticker: string; limit?: number }) {
|
||||
return await apiFetch<{ task: Task }>('/api/filings/sync', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input)
|
||||
});
|
||||
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) {
|
||||
return await apiFetch<{ task: Task }>(`/api/filings/${accessionNumber}/analyze`, {
|
||||
method: 'POST'
|
||||
});
|
||||
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) {
|
||||
return await apiFetch<{ task: Task }>(`/api/tasks/${taskId}`);
|
||||
const result = await client.api.tasks[taskId].get();
|
||||
return await unwrapData<{ task: Task }>(result, 'Unable to fetch task');
|
||||
}
|
||||
|
||||
export async function listRecentTasks(limit = 20) {
|
||||
return await apiFetch<{ tasks: Task[] }>(`/api/tasks?limit=${limit}`);
|
||||
const result = await client.api.tasks.get({
|
||||
$query: {
|
||||
limit
|
||||
}
|
||||
});
|
||||
|
||||
return await unwrapData<{ tasks: Task[] }>(result, 'Unable to fetch tasks');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user