Files
huheng-research/apps/web/src/lib/api.ts

339 lines
8.1 KiB
TypeScript

export type AreaScore = {
area_id: string;
name: string;
district: string;
segment: string;
month: string;
investment_score: number;
recommendation: string;
liquidity_score: number;
momentum_score: number;
rent_support_score: number;
safety_margin_score: number;
credit_support_score: number;
supply_risk_score: number;
valuation_pressure_score: number;
transaction_count: number;
transaction_price_psm: number;
listing_count: number;
listing_price_psm: number;
rent_price_psm: number;
median_days_on_market: number;
annual_rent_yield_pct: number;
listing_pressure_ratio: number;
discount_pct: number;
price_momentum_pct: number;
volume_momentum_pct: number;
};
export type AreaScoreSummary = {
area_id: string;
name: string;
district: string;
investment_score: number;
recommendation: string;
};
export type MarketOverview = {
month: string;
area_count: number;
average_investment_score: number;
average_rent_yield_pct: number;
top_area: AreaScoreSummary | null;
weakest_area: AreaScoreSummary | null;
highest_supply_risk_area: AreaScoreSummary | null;
};
export type NeighborhoodScore = {
neighborhood_id: string;
area_id: string;
name: string;
area_name: string;
district: string;
month: string;
investment_score: number;
recommendation: string;
liquidity_score: number;
rent_support_score: number;
price_safety_score: number;
location_score: number;
building_age_score: number;
transaction_count: number;
transaction_price_psm: number;
listing_count: number;
listing_price_psm: number;
rent_price_psm: number;
median_days_on_market: number;
annual_rent_yield_pct: number;
discount_pct: number;
available_units: number;
};
export type WatchlistItem = {
watchlist_item_id: number;
neighborhood_id: string | null;
area_id: string | null;
target_price_psm: number | null;
status: string;
notes: string;
};
export type CreateWatchlistItem = {
area_id?: string;
neighborhood_id?: string;
target_price_psm?: number;
notes?: string;
};
export type DataSource = {
source_id: number;
name: string;
source_type: string;
url: string | null;
cadence: string | null;
reliability: string;
notes: string;
created_at: string;
};
export type CreateDataSource = {
name: string;
source_type: string;
url?: string;
cadence?: string;
reliability?: string;
notes?: string;
};
export type IngestionRun = {
run_id: number;
source_id: number | null;
source_name: string | null;
raw_artifact_id: number | null;
status: string;
started_at: string;
finished_at: string | null;
raw_uri: string | null;
row_count: number | null;
error_message: string | null;
};
export type CreateIngestionRun = {
source_id?: number;
status?: string;
raw_uri?: string;
raw_artifact_id?: number;
row_count?: number;
error_message?: string;
};
export type FinishIngestionRun = {
status: "succeeded" | "failed";
row_count?: number;
error_message?: string;
};
export type RawArtifact = {
artifact_id: number;
run_id: number | null;
source_id: number | null;
source_name: string | null;
original_filename: string;
raw_uri: string;
sha256: string;
size_bytes: number;
mime_type: string | null;
uploaded_by: string;
notes: string;
created_at: string;
};
export type CreateRawArtifact = {
run_id?: number;
source_id?: number;
original_filename: string;
raw_uri: string;
sha256: string;
size_bytes: number;
mime_type?: string;
uploaded_by?: string;
notes?: string;
};
export type ImportExecutionRequest = {
template_name: string;
run_id: number;
raw_artifact_id: number;
rows: Record<string, string | number | null>[];
allow_reimport?: boolean;
};
export type ImportExecutionResponse = {
template_name: string;
target_table: string;
run_id: number;
raw_artifact_id: number;
row_count: number;
raw_row_count: number;
duplicate_artifact: boolean;
previous_run_ids: number[];
status: string;
};
export type ModelRun = {
model_run_id: number;
model_name: string;
model_version: string;
target_month: string;
status: string;
parameters: Record<string, unknown>;
started_at: string;
finished_at: string | null;
row_count: number | null;
error_message: string | null;
};
export type CreateAreaScoreModelRun = {
month: string;
model_version?: string;
};
export type AreaScoreModelRunResponse = {
model_run: ModelRun;
scores: AreaScore[];
};
export type AreaScoreLineage = {
area_id: string;
area_name: string;
month: string;
investment_score: number;
model_run_id: number | null;
model_version: string;
ingestion_run_id: number | null;
raw_artifact_id: number | null;
raw_uri: string | null;
sha256: string | null;
source_name: string | null;
metric_updated_at: string;
score_computed_at: string;
};
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL?.replace(/\/$/, "") ?? "http://127.0.0.1:8080";
export async function fetchMarketOverview(month: string): Promise<MarketOverview> {
return fetchJson(`/api/v1/market/overview?month=${encodeURIComponent(month)}`);
}
export async function fetchAreaScores(month: string): Promise<AreaScore[]> {
return fetchJson(`/api/v1/areas/scores?month=${encodeURIComponent(month)}`);
}
export async function fetchNeighborhoodScores(month: string): Promise<NeighborhoodScore[]> {
return fetchJson(`/api/v1/neighborhoods/scores?month=${encodeURIComponent(month)}`);
}
export async function fetchWatchlistItems(): Promise<WatchlistItem[]> {
return fetchJson("/api/v1/watchlist");
}
export async function createWatchlistItem(
payload: CreateWatchlistItem,
): Promise<WatchlistItem> {
return fetchJson("/api/v1/watchlist", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function archiveWatchlistItem(id: number): Promise<WatchlistItem> {
return fetchJson(`/api/v1/watchlist/${id}`, {
method: "DELETE",
});
}
export async function fetchDataSources(): Promise<DataSource[]> {
return fetchJson("/api/v1/data-sources");
}
export async function createDataSource(payload: CreateDataSource): Promise<DataSource> {
return fetchJson("/api/v1/data-sources", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchIngestionRuns(): Promise<IngestionRun[]> {
return fetchJson("/api/v1/ingestion-runs");
}
export async function createIngestionRun(payload: CreateIngestionRun): Promise<IngestionRun> {
return fetchJson("/api/v1/ingestion-runs", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function finishIngestionRun(
id: number,
payload: FinishIngestionRun,
): Promise<IngestionRun> {
return fetchJson(`/api/v1/ingestion-runs/${id}/finish`, {
method: "PATCH",
body: JSON.stringify(payload),
});
}
export async function fetchRawArtifacts(): Promise<RawArtifact[]> {
return fetchJson("/api/v1/raw-artifacts");
}
export async function createRawArtifact(payload: CreateRawArtifact): Promise<RawArtifact> {
return fetchJson("/api/v1/raw-artifacts", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function executeImport(
payload: ImportExecutionRequest,
): Promise<ImportExecutionResponse> {
return fetchJson("/api/v1/imports/execute", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function createAreaScoreModelRun(
payload: CreateAreaScoreModelRun,
): Promise<AreaScoreModelRunResponse> {
return fetchJson("/api/v1/model-runs/area-scores", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchAreaScoreLineage(
areaId: string,
month: string,
): Promise<AreaScoreLineage> {
return fetchJson(
`/api/v1/areas/${encodeURIComponent(areaId)}/score-lineage?month=${encodeURIComponent(month)}`,
);
}
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, {
...init,
headers: {
"Content-Type": "application/json",
...init?.headers,
},
});
if (!response.ok) {
throw new Error(`API request failed with ${response.status}`);
}
return response.json() as Promise<T>;
}