/** * Notion Exchange Rate Worker * * Fetches daily exchange rates from Open Exchange Rates API * and updates a Notion database with the latest rates. * * - Run `npm run dev` in your terminal to start a development server * - Run `curl "http://localhost:8787/__scheduled?cron=0+0+*+*+*"` to test the scheduled handler * - Run `npm run deploy` to publish your Worker */ interface OXRResponse { base: string; rates: Record; timestamp: number; } interface NotionQueryResponse { results: Array<{ id: string; properties: { Code: { title: Array<{ plain_text: string }> }; }; }>; has_more: boolean; next_cursor: string | null; } interface UpdateResult { currency: string; success: boolean; error?: string; } interface SyncResult { success: boolean; currenciesFound: number; ratesFetched: number; succeeded: number; failed: number; errors: string[]; } async function fetchExchangeRates(appId: string): Promise { const url = `https://openexchangerates.org/api/latest.json?app_id=${appId}`; const response = await fetch(url); if (!response.ok) { throw new Error(`OXR API error: ${response.status} ${response.statusText}`); } return response.json() as Promise; } async function fetchAllNotionPages( databaseId: string, token: string ): Promise> { const currencyMap = new Map(); let hasMore = true; let nextCursor: string | null = null; while (hasMore) { const body: { page_size: number; start_cursor?: string } = { page_size: 100 }; if (nextCursor) { body.start_cursor = nextCursor; } const response = await fetch(`https://api.notion.com/v1/databases/${databaseId}/query`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Notion-Version': '2022-06-28', }, body: JSON.stringify(body), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Notion query error: ${response.status} - ${errorText}`); } const data = (await response.json()) as NotionQueryResponse; for (const page of data.results) { const code = page.properties.Code.title[0]?.plain_text; if (code) { currencyMap.set(code, page.id); } } hasMore = data.has_more; nextCursor = data.next_cursor; } return currencyMap; } async function updateNotionPage( pageId: string, token: string, rate: number ): Promise { const now = new Date().toISOString(); const response = await fetch(`https://api.notion.com/v1/pages/${pageId}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Notion-Version': '2022-06-28', }, body: JSON.stringify({ properties: { Rate: { number: rate, }, 'Updated At': { date: { start: now, }, }, }, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Notion update error: ${response.status} - ${errorText}`); } } async function syncExchangeRates(env: Env): Promise { const result: SyncResult = { success: false, currenciesFound: 0, ratesFetched: 0, succeeded: 0, failed: 0, errors: [], }; // Validate environment variables if (!env.OXR_APP_ID || !env.NOTION_TOKEN || !env.NOTION_DATABASE_ID) { result.errors.push('Missing required environment variables'); return result; } try { // Step 1: Fetch all existing pages from Notion database console.log('Fetching existing pages from Notion...'); const currencyMap = await fetchAllNotionPages(env.NOTION_DATABASE_ID, env.NOTION_TOKEN); result.currenciesFound = currencyMap.size; console.log(`Found ${currencyMap.size} currencies in Notion database`); if (currencyMap.size === 0) { result.success = true; console.log('No currencies found in Notion database. Nothing to update.'); return result; } // Step 2: Fetch exchange rates from OXR console.log('Fetching exchange rates from OXR...'); const oxrData = await fetchExchangeRates(env.OXR_APP_ID); result.ratesFetched = Object.keys(oxrData.rates).length; console.log(`Fetched ${result.ratesFetched} currency rates. Base: ${oxrData.base}`); // Step 3: Update only currencies that exist in Notion const results: UpdateResult[] = []; for (const [currencyCode, pageId] of currencyMap) { const rate = oxrData.rates[currencyCode]; if (rate === undefined) { console.warn(`No rate found for ${currencyCode} in OXR data`); continue; } try { await updateNotionPage(pageId, env.NOTION_TOKEN, rate); results.push({ currency: currencyCode, success: true }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); results.push({ currency: currencyCode, success: false, error: errorMessage }); result.errors.push(`${currencyCode}: ${errorMessage}`); console.error(`Failed to update ${currencyCode}: ${errorMessage}`); } } // Summary result.succeeded = results.filter((r) => r.success).length; result.failed = results.filter((r) => !r.success).length; result.success = true; console.log(`Sync completed: ${result.succeeded} succeeded, ${result.failed} failed`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); result.errors.push(errorMessage); console.error(`Exchange rate sync failed: ${errorMessage}`); } return result; } export default { async fetch(req, env, ctx): Promise { const url = new URL(req.url); // Manual trigger endpoint if (url.pathname === '/sync') { const result = await syncExchangeRates(env); return new Response(JSON.stringify(result, null, 2), { headers: { 'Content-Type': 'application/json' }, }); } return new Response('Notion Exchange Rate Worker\n\nUse /sync to manually trigger sync.', { headers: { 'Content-Type': 'text/plain' }, }); }, async scheduled(event, env, ctx): Promise { console.log(`Exchange rate sync started at ${event.cron}`); await syncExchangeRates(env); }, } satisfies ExportedHandler;