| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- /**
- * 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<string, number>;
- 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<OXRResponse> {
- 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<OXRResponse>;
- }
- async function fetchAllNotionPages(
- databaseId: string,
- token: string
- ): Promise<Map<string, string>> {
- const currencyMap = new Map<string, string>();
- 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<void> {
- 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<SyncResult> {
- 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<Response> {
- 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<void> {
- console.log(`Exchange rate sync started at ${event.cron}`);
- await syncExchangeRates(env);
- },
- } satisfies ExportedHandler<Env>;
|