TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { desc, eq, sql } from 'drizzle-orm';2import { fxRates } from '@rareindex/database';3import { SUPPORTED_CURRENCIES, logger, toDateOnly } from '@rareindex/shared';4import { db } from './lib/db.ts';56/**7 * FX sync from the ECB reference rates published through frankfurter (https://api.frankfurter.app,8 * an open-data mirror of the ECB euro reference rates). Stored as base USD → quote per 1 USD.9 * Non-ECB currencies (e.g. TWD, AED) stay absent; sales in those currencies keep price_usd NULL10 * until a rate source exists — never approximated.11 */12const API = 'https://api.frankfurter.app';13const log = logger.child({ component: 'fx' });1415interface RangeResponse {16 base: string;17 start_date: string;18 end_date: string;19 rates: Record<string, Record<string, number>>;20}2122async function availableCurrencies(): Promise<string[]> {23 const res = await fetch(`${API}/currencies`, { headers: { 'user-agent': 'RareIndexBot/0.1 (fx sync)' } });24 if (!res.ok) throw new Error(`frankfurter currencies HTTP ${res.status}`);25 const json = (await res.json()) as Record<string, string>;26 return Object.keys(json);27}2829export async function syncFxRange(start: string, end: string): Promise<number> {30 const avail = new Set(await availableCurrencies());31 const quotes = SUPPORTED_CURRENCIES.filter((c) => c !== 'USD' && avail.has(c));32 const res = await fetch(`${API}/${start}..${end}?from=USD&to=${quotes.join(',')}`, { headers: { 'user-agent': 'RareIndexBot/0.1 (fx sync)' } });33 if (!res.ok) throw new Error(`frankfurter range HTTP ${res.status}`);34 const json = (await res.json()) as RangeResponse;35 const rows: Array<typeof fxRates.$inferInsert> = [];36 for (const [date, byQuote] of Object.entries(json.rates ?? {})) {37 for (const [quote, rate] of Object.entries(byQuote)) rows.push({ date, base: 'USD', quote, rate, source: 'ecb' });38 }39 for (let i = 0; i < rows.length; i += 1000) {40 await db()41 .insert(fxRates)42 .values(rows.slice(i, i + 1000))43 .onConflictDoUpdate({ target: [fxRates.date, fxRates.base, fxRates.quote], set: { rate: sql`excluded.rate` } });44 }45 return rows.length;46}4748/** Daily sync: from the last stored date (or 30 days back) to today. `backfill` loads 1999→today in yearly chunks. */49export async function syncFx(opts: { backfill?: boolean } = {}): Promise<{ rows: number; from: string; to: string }> {50 const today = toDateOnly(new Date());51 let from: string;52 if (opts.backfill) from = '1999-01-04';53 else {54 const [last] = await db().select({ date: fxRates.date }).from(fxRates).where(eq(fxRates.base, 'USD')).orderBy(desc(fxRates.date)).limit(1);55 from = last ? last.date : toDateOnly(new Date(Date.now() - 30 * 86_400_000));56 }57 let total = 0;58 let cursor = from;59 while (cursor <= today) {60 const year = Number(cursor.slice(0, 4));61 const end = `${year}-12-31` < today ? `${year}-12-31` : today;62 const n = await syncFxRange(cursor, end);63 total += n;64 log.info({ from: cursor, to: end, rows: n }, 'fx range synced');65 cursor = `${year + 1}-01-01`;66 }67 return { rows: total, from, to: today };68}69