|
@@ -15,11 +15,20 @@ interface OXRResponse {
|
|
|
timestamp: number;
|
|
timestamp: number;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface NotionPage {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ code: string;
|
|
|
|
|
+ rate: number | null;
|
|
|
|
|
+ updatedAt: string | null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
interface NotionQueryResponse {
|
|
interface NotionQueryResponse {
|
|
|
results: Array<{
|
|
results: Array<{
|
|
|
id: string;
|
|
id: string;
|
|
|
properties: {
|
|
properties: {
|
|
|
Code: { title: Array<{ plain_text: string }> };
|
|
Code: { title: Array<{ plain_text: string }> };
|
|
|
|
|
+ Rate: { number: number | null };
|
|
|
|
|
+ 'Updated At': { date: { start: string } | null };
|
|
|
};
|
|
};
|
|
|
}>;
|
|
}>;
|
|
|
has_more: boolean;
|
|
has_more: boolean;
|
|
@@ -48,10 +57,16 @@ interface ConvertResult {
|
|
|
amount: number;
|
|
amount: number;
|
|
|
rate: number;
|
|
rate: number;
|
|
|
result: number;
|
|
result: number;
|
|
|
- timestamp: number;
|
|
|
|
|
|
|
+ timestamp: string | null;
|
|
|
error?: string;
|
|
error?: string;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface RatesResult {
|
|
|
|
|
+ base: string;
|
|
|
|
|
+ timestamp: string | null;
|
|
|
|
|
+ rates: Record<string, number>;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function fetchExchangeRates(appId: string): Promise<OXRResponse> {
|
|
async function fetchExchangeRates(appId: string): Promise<OXRResponse> {
|
|
|
const url = `https://openexchangerates.org/api/latest.json?app_id=${appId}`;
|
|
const url = `https://openexchangerates.org/api/latest.json?app_id=${appId}`;
|
|
|
const response = await fetch(url);
|
|
const response = await fetch(url);
|
|
@@ -63,11 +78,11 @@ async function fetchExchangeRates(appId: string): Promise<OXRResponse> {
|
|
|
return response.json() as Promise<OXRResponse>;
|
|
return response.json() as Promise<OXRResponse>;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function fetchAllNotionPages(
|
|
|
|
|
|
|
+async function fetchNotionRates(
|
|
|
databaseId: string,
|
|
databaseId: string,
|
|
|
token: string
|
|
token: string
|
|
|
-): Promise<Map<string, string>> {
|
|
|
|
|
- const currencyMap = new Map<string, string>();
|
|
|
|
|
|
|
+): Promise<NotionPage[]> {
|
|
|
|
|
+ const pages: NotionPage[] = [];
|
|
|
let hasMore = true;
|
|
let hasMore = true;
|
|
|
let nextCursor: string | null = null;
|
|
let nextCursor: string | null = null;
|
|
|
|
|
|
|
@@ -97,7 +112,12 @@ async function fetchAllNotionPages(
|
|
|
for (const page of data.results) {
|
|
for (const page of data.results) {
|
|
|
const code = page.properties.Code.title[0]?.plain_text;
|
|
const code = page.properties.Code.title[0]?.plain_text;
|
|
|
if (code) {
|
|
if (code) {
|
|
|
- currencyMap.set(code, page.id);
|
|
|
|
|
|
|
+ pages.push({
|
|
|
|
|
+ id: page.id,
|
|
|
|
|
+ code,
|
|
|
|
|
+ rate: page.properties.Rate.number,
|
|
|
|
|
+ updatedAt: page.properties['Updated At'].date?.start || null,
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -105,7 +125,7 @@ async function fetchAllNotionPages(
|
|
|
nextCursor = data.next_cursor;
|
|
nextCursor = data.next_cursor;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return currencyMap;
|
|
|
|
|
|
|
+ return pages;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function updateNotionPage(
|
|
async function updateNotionPage(
|
|
@@ -152,54 +172,49 @@ async function syncExchangeRates(env: Env): Promise<SyncResult> {
|
|
|
errors: [],
|
|
errors: [],
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // Validate environment variables
|
|
|
|
|
if (!env.OXR_APP_ID || !env.NOTION_TOKEN || !env.NOTION_DATABASE_ID) {
|
|
if (!env.OXR_APP_ID || !env.NOTION_TOKEN || !env.NOTION_DATABASE_ID) {
|
|
|
result.errors.push('Missing required environment variables');
|
|
result.errors.push('Missing required environment variables');
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // Step 1: Fetch all existing pages from Notion database
|
|
|
|
|
console.log('Fetching existing pages from Notion...');
|
|
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`);
|
|
|
|
|
|
|
+ const notionPages = await fetchNotionRates(env.NOTION_DATABASE_ID, env.NOTION_TOKEN);
|
|
|
|
|
+ result.currenciesFound = notionPages.length;
|
|
|
|
|
+ console.log(`Found ${notionPages.length} currencies in Notion database`);
|
|
|
|
|
|
|
|
- if (currencyMap.size === 0) {
|
|
|
|
|
|
|
+ if (notionPages.length === 0) {
|
|
|
result.success = true;
|
|
result.success = true;
|
|
|
console.log('No currencies found in Notion database. Nothing to update.');
|
|
console.log('No currencies found in Notion database. Nothing to update.');
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Step 2: Fetch exchange rates from OXR
|
|
|
|
|
console.log('Fetching exchange rates from OXR...');
|
|
console.log('Fetching exchange rates from OXR...');
|
|
|
const oxrData = await fetchExchangeRates(env.OXR_APP_ID);
|
|
const oxrData = await fetchExchangeRates(env.OXR_APP_ID);
|
|
|
result.ratesFetched = Object.keys(oxrData.rates).length;
|
|
result.ratesFetched = Object.keys(oxrData.rates).length;
|
|
|
console.log(`Fetched ${result.ratesFetched} currency rates. Base: ${oxrData.base}`);
|
|
console.log(`Fetched ${result.ratesFetched} currency rates. Base: ${oxrData.base}`);
|
|
|
|
|
|
|
|
- // Step 3: Update only currencies that exist in Notion
|
|
|
|
|
const results: UpdateResult[] = [];
|
|
const results: UpdateResult[] = [];
|
|
|
|
|
|
|
|
- for (const [currencyCode, pageId] of currencyMap) {
|
|
|
|
|
- const rate = oxrData.rates[currencyCode];
|
|
|
|
|
|
|
+ for (const page of notionPages) {
|
|
|
|
|
+ const rate = oxrData.rates[page.code];
|
|
|
|
|
|
|
|
if (rate === undefined) {
|
|
if (rate === undefined) {
|
|
|
- console.warn(`No rate found for ${currencyCode} in OXR data`);
|
|
|
|
|
|
|
+ console.warn(`No rate found for ${page.code} in OXR data`);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- await updateNotionPage(pageId, env.NOTION_TOKEN, rate);
|
|
|
|
|
- results.push({ currency: currencyCode, success: true });
|
|
|
|
|
|
|
+ await updateNotionPage(page.id, env.NOTION_TOKEN, rate);
|
|
|
|
|
+ results.push({ currency: page.code, success: true });
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
const errorMessage = error instanceof Error ? error.message : String(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}`);
|
|
|
|
|
|
|
+ results.push({ currency: page.code, success: false, error: errorMessage });
|
|
|
|
|
+ result.errors.push(`${page.code}: ${errorMessage}`);
|
|
|
|
|
+ console.error(`Failed to update ${page.code}: ${errorMessage}`);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Summary
|
|
|
|
|
result.succeeded = results.filter((r) => r.success).length;
|
|
result.succeeded = results.filter((r) => r.success).length;
|
|
|
result.failed = results.filter((r) => !r.success).length;
|
|
result.failed = results.filter((r) => !r.success).length;
|
|
|
result.success = true;
|
|
result.success = true;
|
|
@@ -213,6 +228,32 @@ async function syncExchangeRates(env: Env): Promise<SyncResult> {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function getRatesFromNotion(env: Env): Promise<RatesResult> {
|
|
|
|
|
+ if (!env.NOTION_TOKEN || !env.NOTION_DATABASE_ID) {
|
|
|
|
|
+ throw new Error('Missing NOTION_TOKEN or NOTION_DATABASE_ID');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const pages = await fetchNotionRates(env.NOTION_DATABASE_ID, env.NOTION_TOKEN);
|
|
|
|
|
+
|
|
|
|
|
+ const rates: Record<string, number> = {};
|
|
|
|
|
+ let latestTimestamp: string | null = null;
|
|
|
|
|
+
|
|
|
|
|
+ for (const page of pages) {
|
|
|
|
|
+ if (page.rate !== null) {
|
|
|
|
|
+ rates[page.code] = page.rate;
|
|
|
|
|
+ if (page.updatedAt && (!latestTimestamp || page.updatedAt > latestTimestamp)) {
|
|
|
|
|
+ latestTimestamp = page.updatedAt;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ base: 'USD',
|
|
|
|
|
+ timestamp: latestTimestamp,
|
|
|
|
|
+ rates,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function convertCurrency(
|
|
async function convertCurrency(
|
|
|
env: Env,
|
|
env: Env,
|
|
|
from: string,
|
|
from: string,
|
|
@@ -226,20 +267,15 @@ async function convertCurrency(
|
|
|
amount,
|
|
amount,
|
|
|
rate: 0,
|
|
rate: 0,
|
|
|
result: 0,
|
|
result: 0,
|
|
|
- timestamp: 0,
|
|
|
|
|
|
|
+ timestamp: null,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- if (!env.OXR_APP_ID) {
|
|
|
|
|
- result.error = 'Missing OXR_APP_ID';
|
|
|
|
|
- return result;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
- const oxrData = await fetchExchangeRates(env.OXR_APP_ID);
|
|
|
|
|
- result.timestamp = oxrData.timestamp * 1000;
|
|
|
|
|
|
|
+ const ratesData = await getRatesFromNotion(env);
|
|
|
|
|
+ result.timestamp = ratesData.timestamp;
|
|
|
|
|
|
|
|
- const fromRate = oxrData.rates[from.toUpperCase()];
|
|
|
|
|
- const toRate = oxrData.rates[to.toUpperCase()];
|
|
|
|
|
|
|
+ const fromRate = ratesData.rates[from.toUpperCase()];
|
|
|
|
|
+ const toRate = ratesData.rates[to.toUpperCase()];
|
|
|
|
|
|
|
|
if (!fromRate) {
|
|
if (!fromRate) {
|
|
|
result.error = `Unknown currency: ${from}`;
|
|
result.error = `Unknown currency: ${from}`;
|
|
@@ -282,7 +318,7 @@ export default {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Currency conversion endpoint
|
|
|
|
|
|
|
+ // Currency conversion endpoint (data from Notion)
|
|
|
if (url.pathname === '/convert') {
|
|
if (url.pathname === '/convert') {
|
|
|
const from = url.searchParams.get('from');
|
|
const from = url.searchParams.get('from');
|
|
|
const to = url.searchParams.get('to');
|
|
const to = url.searchParams.get('to');
|
|
@@ -318,38 +354,23 @@ export default {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Rates endpoint - get all rates for a base currency
|
|
|
|
|
|
|
+ // Rates endpoint - get all rates from Notion database
|
|
|
if (url.pathname === '/rates') {
|
|
if (url.pathname === '/rates') {
|
|
|
const base = url.searchParams.get('base') || 'USD';
|
|
const base = url.searchParams.get('base') || 'USD';
|
|
|
|
|
|
|
|
- if (!env.OXR_APP_ID) {
|
|
|
|
|
- return new Response(
|
|
|
|
|
- JSON.stringify({ error: 'Missing OXR_APP_ID' }),
|
|
|
|
|
- { status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
- const oxrData = await fetchExchangeRates(env.OXR_APP_ID);
|
|
|
|
|
|
|
+ const ratesData = await getRatesFromNotion(env);
|
|
|
|
|
|
|
|
// If base is USD, return as-is
|
|
// If base is USD, return as-is
|
|
|
if (base.toUpperCase() === 'USD') {
|
|
if (base.toUpperCase() === 'USD') {
|
|
|
return new Response(
|
|
return new Response(
|
|
|
- JSON.stringify(
|
|
|
|
|
- {
|
|
|
|
|
- base: oxrData.base,
|
|
|
|
|
- timestamp: oxrData.timestamp * 1000,
|
|
|
|
|
- rates: oxrData.rates,
|
|
|
|
|
- },
|
|
|
|
|
- null,
|
|
|
|
|
- 2
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ JSON.stringify(ratesData, null, 2),
|
|
|
{ headers: { 'Content-Type': 'application/json' } }
|
|
{ headers: { 'Content-Type': 'application/json' } }
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, recalculate rates for the new base
|
|
// Otherwise, recalculate rates for the new base
|
|
|
- const baseRate = oxrData.rates[base.toUpperCase()];
|
|
|
|
|
|
|
+ const baseRate = ratesData.rates[base.toUpperCase()];
|
|
|
if (!baseRate) {
|
|
if (!baseRate) {
|
|
|
return new Response(
|
|
return new Response(
|
|
|
JSON.stringify({ error: `Unknown currency: ${base}` }),
|
|
JSON.stringify({ error: `Unknown currency: ${base}` }),
|
|
@@ -358,7 +379,7 @@ export default {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const convertedRates: Record<string, number> = {};
|
|
const convertedRates: Record<string, number> = {};
|
|
|
- for (const [currency, rate] of Object.entries(oxrData.rates)) {
|
|
|
|
|
|
|
+ for (const [currency, rate] of Object.entries(ratesData.rates)) {
|
|
|
convertedRates[currency] = rate / baseRate;
|
|
convertedRates[currency] = rate / baseRate;
|
|
|
}
|
|
}
|
|
|
convertedRates['USD'] = 1 / baseRate;
|
|
convertedRates['USD'] = 1 / baseRate;
|
|
@@ -367,7 +388,7 @@ export default {
|
|
|
JSON.stringify(
|
|
JSON.stringify(
|
|
|
{
|
|
{
|
|
|
base: base.toUpperCase(),
|
|
base: base.toUpperCase(),
|
|
|
- timestamp: oxrData.timestamp * 1000,
|
|
|
|
|
|
|
+ timestamp: ratesData.timestamp,
|
|
|
rates: convertedRates,
|
|
rates: convertedRates,
|
|
|
},
|
|
},
|
|
|
null,
|
|
null,
|
|
@@ -389,9 +410,9 @@ export default {
|
|
|
`Notion Exchange Rate Worker
|
|
`Notion Exchange Rate Worker
|
|
|
|
|
|
|
|
API Endpoints:
|
|
API Endpoints:
|
|
|
- GET /convert?from=USD&to=CNY&amount=100 - Convert currency
|
|
|
|
|
- GET /rates?base=USD - Get exchange rates
|
|
|
|
|
- GET /sync - Sync rates to Notion`,
|
|
|
|
|
|
|
+ GET /convert?from=USD&to=CNY&amount=100 - Convert currency (data from Notion)
|
|
|
|
|
+ GET /rates?base=USD - Get exchange rates (data from Notion)
|
|
|
|
|
+ GET /sync - Sync rates from OXR to Notion`,
|
|
|
{ headers: { 'Content-Type': 'text/plain' } }
|
|
{ headers: { 'Content-Type': 'text/plain' } }
|
|
|
);
|
|
);
|
|
|
},
|
|
},
|