From e8fbe76f2eb10c4bbd43a8be7f4c7a7dd895994f Mon Sep 17 00:00:00 2001 From: Boki Date: Mon, 16 Jun 2025 09:24:17 -0400 Subject: [PATCH] finished exchanges api connections --- .../src/routes/exchange.routes.ts | 137 ++++++++++++++++++ .../src/features/exchanges/ExchangesPage.tsx | 110 +++++++++++++- .../exchanges/components/AddSourceDialog.tsx | 22 ++- .../exchanges/components/ExchangesTable.tsx | 36 +++-- .../features/exchanges/hooks/useExchanges.ts | 23 +++ .../web/src/features/exchanges/types/index.ts | 1 + 6 files changed, 306 insertions(+), 23 deletions(-) diff --git a/apps/data-service/src/routes/exchange.routes.ts b/apps/data-service/src/routes/exchange.routes.ts index 081c54b..addbf59 100644 --- a/apps/data-service/src/routes/exchange.routes.ts +++ b/apps/data-service/src/routes/exchange.routes.ts @@ -183,3 +183,140 @@ exchangeRoutes.post('/api/exchanges/sync', async c => { return c.json({ error: 'Internal server error' }, 500); } }); + +// Update exchange (shortName and active status) +exchangeRoutes.patch('/api/exchanges/:id', async c => { + try { + const id = c.req.param('id'); + const updates = await c.req.json(); + + // Validate the updates - only allow shortName and active + const sanitizedUpdates: Record = {}; + + if ('shortName' in updates && typeof updates.shortName === 'string') { + sanitizedUpdates.shortName = updates.shortName; + } + if ('active' in updates && typeof updates.active === 'boolean') { + sanitizedUpdates.active = updates.active; + } + + if (Object.keys(sanitizedUpdates).length === 0) { + return c.json({ error: 'No valid fields to update' }, 400); + } + + await connectMongoDB(); + const db = getDatabase(); + const collection = db.collection('masterExchanges'); + + // Update using MongoDB _id (ObjectId) + const result = await collection.updateOne( + { _id: new (await import('mongodb')).ObjectId(id) }, + { + $set: { + ...sanitizedUpdates, + updated_at: new Date(), + }, + } + ); + + if (result.matchedCount === 0) { + return c.json({ error: 'Exchange not found' }, 404); + } + + return c.json({ + status: 'success', + message: 'Exchange updated successfully', + data: { id, updates: sanitizedUpdates }, + }); + } catch (error) { + logger.error('Error updating exchange', { error }); + return c.json({ error: 'Internal server error' }, 500); + } +}); + +// Add source to exchange +exchangeRoutes.post('/api/exchanges/:id/sources', async c => { + try { + const id = c.req.param('id'); + const { source, source_code, mapping } = await c.req.json(); + + if (!source || !source_code || !mapping || !mapping.id) { + return c.json({ error: 'source, source_code, and mapping with id are required' }, 400); + } + + // Create the storage key using source and source_code + const storageKey = `${source}_${source_code}`; + + // Add lastUpdated to the mapping + const sourceData = { + ...mapping, + lastUpdated: new Date(), + }; + + await connectMongoDB(); + const db = getDatabase(); + const collection = db.collection('masterExchanges'); + + // Update using MongoDB _id (ObjectId) + const result = await collection.updateOne( + { _id: new (await import('mongodb')).ObjectId(id) }, + { + $set: { + [`sourceMappings.${storageKey}`]: sourceData, + updated_at: new Date(), + }, + } + ); + + if (result.matchedCount === 0) { + return c.json({ error: 'Exchange not found' }, 404); + } + + return c.json({ + status: 'success', + message: 'Source mapping added successfully', + data: { id, storageKey, mapping: sourceData }, + }); + } catch (error) { + logger.error('Error adding source mapping', { error }); + return c.json({ error: 'Internal server error' }, 500); + } +}); + +// Remove source from exchange +exchangeRoutes.delete('/api/exchanges/:id/sources/:sourceName', async c => { + try { + const id = c.req.param('id'); + const sourceName = c.req.param('sourceName'); + + await connectMongoDB(); + const db = getDatabase(); + const collection = db.collection('masterExchanges'); + + // Remove the source mapping using MongoDB _id (ObjectId) + const result = await collection.updateOne( + { _id: new (await import('mongodb')).ObjectId(id) }, + { + $unset: { + [`sourceMappings.${sourceName}`]: '', + }, + $set: { + updated_at: new Date(), + }, + } + ); + + if (result.matchedCount === 0) { + return c.json({ error: 'Exchange not found' }, 404); + } + + return c.json({ + status: 'success', + message: 'Source mapping removed successfully', + data: { id, sourceName }, + }); + } catch (error) { + logger.error('Error removing source mapping', { error }); + return c.json({ error: 'Internal server error' }, 500); + } +}); diff --git a/apps/web/src/features/exchanges/ExchangesPage.tsx b/apps/web/src/features/exchanges/ExchangesPage.tsx index 4dc71cc..3845940 100644 --- a/apps/web/src/features/exchanges/ExchangesPage.tsx +++ b/apps/web/src/features/exchanges/ExchangesPage.tsx @@ -1,15 +1,115 @@ +import { ArrowPathIcon, CheckCircleIcon, ExclamationTriangleIcon, XMarkIcon } from '@heroicons/react/24/outline'; +import { useEffect, useState } from 'react'; import { ExchangesTable } from './components/ExchangesTable'; +import { useExchanges } from './hooks/useExchanges'; export function ExchangesPage() { + const { syncExchanges } = useExchanges(); + const [syncing, setSyncing] = useState(false); + const [syncStatus, setSyncStatus] = useState<{ + type: 'success' | 'error' | null; + message: string; + }>({ type: null, message: '' }); + + // Auto-dismiss success messages after 5 seconds + useEffect(() => { + if (syncStatus.type === 'success') { + const timer = setTimeout(() => { + setSyncStatus({ type: null, message: '' }); + }, 5000); + return () => clearTimeout(timer); + } + }, [syncStatus.type]); + + const handleSync = async () => { + setSyncing(true); + setSyncStatus({ type: null, message: '' }); + + try { + const result = await syncExchanges(); + if (result) { + setSyncStatus({ + type: 'success', + message: `Exchange sync completed successfully! Job ID: ${result.jobId || 'Unknown'}`, + }); + } else { + setSyncStatus({ + type: 'error', + message: 'Sync failed - no result returned from server', + }); + } + } catch (error) { + setSyncStatus({ + type: 'error', + message: error instanceof Error ? error.message : 'Sync failed with unknown error', + }); + } finally { + setSyncing(false); + } + }; + return (
-
-

Exchange Management

-

- Configure and manage master exchanges with their data sources and providers. -

+
+
+

Exchange Management

+

+ Configure and manage master exchanges with their data sources and providers. +

+
+
+ {syncing && ( +
+
+ + + Syncing exchanges from Interactive Brokers data... + +
+
+ )} + + {syncStatus.type && ( +
+
+
+ {syncStatus.type === 'success' ? ( + + ) : ( + + )} +
+

+ {syncStatus.type === 'success' ? 'Sync Completed' : 'Sync Failed'} +

+

+ {syncStatus.message} +

+
+ +
+
+
+ )} +
); diff --git a/apps/web/src/features/exchanges/components/AddSourceDialog.tsx b/apps/web/src/features/exchanges/components/AddSourceDialog.tsx index 70d2f57..7bc7813 100644 --- a/apps/web/src/features/exchanges/components/AddSourceDialog.tsx +++ b/apps/web/src/features/exchanges/components/AddSourceDialog.tsx @@ -15,10 +15,10 @@ export function AddSourceDialog({ isOpen, onClose, onAddSource, - exchangeId, exchangeName, }: AddSourceDialogProps) { const [source, setSource] = useState(''); + const [sourceCode, setSourceCode] = useState(''); const [id, setId] = useState(''); const [name, setName] = useState(''); const [code, setCode] = useState(''); @@ -27,12 +27,13 @@ export function AddSourceDialog({ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!source || !id || !name || !code) return; + if (!source || !sourceCode || !id || !name || !code) return; setLoading(true); try { await onAddSource({ source, + source_code: sourceCode, mapping: { id, name, @@ -46,6 +47,7 @@ export function AddSourceDialog({ // Reset form setSource(''); + setSourceCode(''); setId(''); setName(''); setCode(''); @@ -116,6 +118,20 @@ export function AddSourceDialog({
+
+ + setSourceCode(e.target.value)} + className="w-full bg-surface border border-border rounded px-3 py-2 text-text-primary focus:ring-1 focus:ring-primary-500 focus:border-primary-500" + placeholder="e.g., IB, ALP, POLY" + required + /> +
+
); @@ -278,6 +283,7 @@ export function ExchangesTable() { onClose={() => setAddSourceDialog(null)} onAddSource={async (sourceRequest: { source: string; + source_code: string; mapping: { id: string; name: string; code: string; aliases: string[] }; }) => { const success = await addSource(addSourceDialog.id, sourceRequest); diff --git a/apps/web/src/features/exchanges/hooks/useExchanges.ts b/apps/web/src/features/exchanges/hooks/useExchanges.ts index b0ef638..d23b7ff 100644 --- a/apps/web/src/features/exchanges/hooks/useExchanges.ts +++ b/apps/web/src/features/exchanges/hooks/useExchanges.ts @@ -116,6 +116,28 @@ export function useExchanges() { [fetchExchanges] ); + const syncExchanges = useCallback(async () => { + try { + const response = await fetch(`${API_BASE_URL}/exchanges/sync`, { + method: 'POST', + }); + + if (!response.ok) { + throw new Error(`Failed to sync exchanges: ${response.statusText}`); + } + + const result = await response.json(); + + // Refresh the exchanges list after sync + await fetchExchanges(); + return result; + } catch (err) { + console.error('Error syncing exchanges:', err); + setError(err instanceof Error ? err.message : 'Failed to sync exchanges'); + return null; + } + }, [fetchExchanges]); + useEffect(() => { fetchExchanges(); }, [fetchExchanges]); @@ -128,5 +150,6 @@ export function useExchanges() { updateExchange, addSource, removeSource, + syncExchanges, }; } diff --git a/apps/web/src/features/exchanges/types/index.ts b/apps/web/src/features/exchanges/types/index.ts index 7c9574c..d48e58d 100644 --- a/apps/web/src/features/exchanges/types/index.ts +++ b/apps/web/src/features/exchanges/types/index.ts @@ -36,6 +36,7 @@ export interface UpdateExchangeRequest { export interface AddSourceRequest { source: string; + source_code: string; mapping: { id: string; name: string;