|
|
@@ -10,217 +10,217 @@
|
|
|
*/
|
|
|
|
|
|
interface OXRResponse {
|
|
|
- base: string;
|
|
|
- rates: Record<string, number>;
|
|
|
- timestamp: number;
|
|
|
+ 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;
|
|
|
+ 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;
|
|
|
+ currency: string;
|
|
|
+ success: boolean;
|
|
|
+ error?: string;
|
|
|
}
|
|
|
|
|
|
interface SyncResult {
|
|
|
- success: boolean;
|
|
|
- currenciesFound: number;
|
|
|
- ratesFetched: number;
|
|
|
- succeeded: number;
|
|
|
- failed: number;
|
|
|
- errors: string[];
|
|
|
+ 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);
|
|
|
+ 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}`);
|
|
|
- }
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`OXR API error: ${response.status} ${response.statusText}`);
|
|
|
+ }
|
|
|
|
|
|
- return response.json() as Promise<OXRResponse>;
|
|
|
+ return response.json() as Promise<OXRResponse>;
|
|
|
}
|
|
|
|
|
|
async function fetchAllNotionPages(
|
|
|
- databaseId: string,
|
|
|
- token: string
|
|
|
+ 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;
|
|
|
+ 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
|
|
|
+ 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}`);
|
|
|
- }
|
|
|
+ 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;
|
|
|
+ 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);
|
|
|
- },
|
|
|
+ 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>;
|