index.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. /**
  2. * Notion Exchange Rate Worker
  3. *
  4. * Fetches daily exchange rates from Open Exchange Rates API
  5. * and updates a Notion database with the latest rates.
  6. *
  7. * - Run `npm run dev` in your terminal to start a development server
  8. * - Run `curl "http://localhost:8787/__scheduled?cron=0+0+*+*+*"` to test the scheduled handler
  9. * - Run `npm run deploy` to publish your Worker
  10. */
  11. interface OXRResponse {
  12. base: string;
  13. rates: Record<string, number>;
  14. timestamp: number;
  15. }
  16. interface NotionQueryResponse {
  17. results: Array<{
  18. id: string;
  19. properties: {
  20. Code: { title: Array<{ plain_text: string }> };
  21. };
  22. }>;
  23. has_more: boolean;
  24. next_cursor: string | null;
  25. }
  26. interface UpdateResult {
  27. currency: string;
  28. success: boolean;
  29. error?: string;
  30. }
  31. interface SyncResult {
  32. success: boolean;
  33. currenciesFound: number;
  34. ratesFetched: number;
  35. succeeded: number;
  36. failed: number;
  37. errors: string[];
  38. }
  39. async function fetchExchangeRates(appId: string): Promise<OXRResponse> {
  40. const url = `https://openexchangerates.org/api/latest.json?app_id=${appId}`;
  41. const response = await fetch(url);
  42. if (!response.ok) {
  43. throw new Error(`OXR API error: ${response.status} ${response.statusText}`);
  44. }
  45. return response.json() as Promise<OXRResponse>;
  46. }
  47. async function fetchAllNotionPages(
  48. databaseId: string,
  49. token: string
  50. ): Promise<Map<string, string>> {
  51. const currencyMap = new Map<string, string>();
  52. let hasMore = true;
  53. let nextCursor: string | null = null;
  54. while (hasMore) {
  55. const body: { page_size: number; start_cursor?: string } = { page_size: 100 };
  56. if (nextCursor) {
  57. body.start_cursor = nextCursor;
  58. }
  59. const response = await fetch(`https://api.notion.com/v1/databases/${databaseId}/query`, {
  60. method: 'POST',
  61. headers: {
  62. 'Authorization': `Bearer ${token}`,
  63. 'Content-Type': 'application/json',
  64. 'Notion-Version': '2022-06-28',
  65. },
  66. body: JSON.stringify(body),
  67. });
  68. if (!response.ok) {
  69. const errorText = await response.text();
  70. throw new Error(`Notion query error: ${response.status} - ${errorText}`);
  71. }
  72. const data = (await response.json()) as NotionQueryResponse;
  73. for (const page of data.results) {
  74. const code = page.properties.Code.title[0]?.plain_text;
  75. if (code) {
  76. currencyMap.set(code, page.id);
  77. }
  78. }
  79. hasMore = data.has_more;
  80. nextCursor = data.next_cursor;
  81. }
  82. return currencyMap;
  83. }
  84. async function updateNotionPage(
  85. pageId: string,
  86. token: string,
  87. rate: number
  88. ): Promise<void> {
  89. const now = new Date().toISOString();
  90. const response = await fetch(`https://api.notion.com/v1/pages/${pageId}`, {
  91. method: 'PATCH',
  92. headers: {
  93. 'Authorization': `Bearer ${token}`,
  94. 'Content-Type': 'application/json',
  95. 'Notion-Version': '2022-06-28',
  96. },
  97. body: JSON.stringify({
  98. properties: {
  99. Rate: {
  100. number: rate,
  101. },
  102. 'Updated At': {
  103. date: {
  104. start: now,
  105. },
  106. },
  107. },
  108. }),
  109. });
  110. if (!response.ok) {
  111. const errorText = await response.text();
  112. throw new Error(`Notion update error: ${response.status} - ${errorText}`);
  113. }
  114. }
  115. async function syncExchangeRates(env: Env): Promise<SyncResult> {
  116. const result: SyncResult = {
  117. success: false,
  118. currenciesFound: 0,
  119. ratesFetched: 0,
  120. succeeded: 0,
  121. failed: 0,
  122. errors: [],
  123. };
  124. // Validate environment variables
  125. if (!env.OXR_APP_ID || !env.NOTION_TOKEN || !env.NOTION_DATABASE_ID) {
  126. result.errors.push('Missing required environment variables');
  127. return result;
  128. }
  129. try {
  130. // Step 1: Fetch all existing pages from Notion database
  131. console.log('Fetching existing pages from Notion...');
  132. const currencyMap = await fetchAllNotionPages(env.NOTION_DATABASE_ID, env.NOTION_TOKEN);
  133. result.currenciesFound = currencyMap.size;
  134. console.log(`Found ${currencyMap.size} currencies in Notion database`);
  135. if (currencyMap.size === 0) {
  136. result.success = true;
  137. console.log('No currencies found in Notion database. Nothing to update.');
  138. return result;
  139. }
  140. // Step 2: Fetch exchange rates from OXR
  141. console.log('Fetching exchange rates from OXR...');
  142. const oxrData = await fetchExchangeRates(env.OXR_APP_ID);
  143. result.ratesFetched = Object.keys(oxrData.rates).length;
  144. console.log(`Fetched ${result.ratesFetched} currency rates. Base: ${oxrData.base}`);
  145. // Step 3: Update only currencies that exist in Notion
  146. const results: UpdateResult[] = [];
  147. for (const [currencyCode, pageId] of currencyMap) {
  148. const rate = oxrData.rates[currencyCode];
  149. if (rate === undefined) {
  150. console.warn(`No rate found for ${currencyCode} in OXR data`);
  151. continue;
  152. }
  153. try {
  154. await updateNotionPage(pageId, env.NOTION_TOKEN, rate);
  155. results.push({ currency: currencyCode, success: true });
  156. } catch (error) {
  157. const errorMessage = error instanceof Error ? error.message : String(error);
  158. results.push({ currency: currencyCode, success: false, error: errorMessage });
  159. result.errors.push(`${currencyCode}: ${errorMessage}`);
  160. console.error(`Failed to update ${currencyCode}: ${errorMessage}`);
  161. }
  162. }
  163. // Summary
  164. result.succeeded = results.filter((r) => r.success).length;
  165. result.failed = results.filter((r) => !r.success).length;
  166. result.success = true;
  167. console.log(`Sync completed: ${result.succeeded} succeeded, ${result.failed} failed`);
  168. } catch (error) {
  169. const errorMessage = error instanceof Error ? error.message : String(error);
  170. result.errors.push(errorMessage);
  171. console.error(`Exchange rate sync failed: ${errorMessage}`);
  172. }
  173. return result;
  174. }
  175. export default {
  176. async fetch(req, env, ctx): Promise<Response> {
  177. const url = new URL(req.url);
  178. // Manual trigger endpoint
  179. if (url.pathname === '/sync') {
  180. const result = await syncExchangeRates(env);
  181. return new Response(JSON.stringify(result, null, 2), {
  182. headers: { 'Content-Type': 'application/json' },
  183. });
  184. }
  185. return new Response('Notion Exchange Rate Worker\n\nUse /sync to manually trigger sync.', {
  186. headers: { 'Content-Type': 'text/plain' },
  187. });
  188. },
  189. async scheduled(event, env, ctx): Promise<void> {
  190. console.log(`Exchange rate sync started at ${event.cron}`);
  191. await syncExchangeRates(env);
  192. },
  193. } satisfies ExportedHandler<Env>;