Intial Version
This commit is contained in:
55
backend/src/api/routes/index.ts
Normal file
55
backend/src/api/routes/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { PriceRepository } from '../../database/repository';
|
||||
|
||||
const repository = new PriceRepository();
|
||||
|
||||
export const indexRoutes: FastifyPluginAsync = async (server) => {
|
||||
// GET /api/index/history?range=7d|30d|90d|all
|
||||
server.get<{
|
||||
Querystring: { range?: string };
|
||||
}>('/history', async (request, reply) => {
|
||||
try {
|
||||
const { range = '7d' } = request.query;
|
||||
const history = await repository.getIndexHistory(range);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: history.map((h: any) => ({
|
||||
timestamp: h.timestamp,
|
||||
price: h.lowestPrice.toString(),
|
||||
vendor: h.vendor,
|
||||
seller: h.sellerName,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
reply.code(500);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// WebSocket endpoint for real-time updates
|
||||
server.get('/ws', { websocket: true }, (connection: any, request: any) => {
|
||||
connection.socket.on('message', (message: any) => {
|
||||
// Echo back for now - will be replaced with real-time updates
|
||||
connection.socket.send(message.toString());
|
||||
});
|
||||
|
||||
// Send initial data
|
||||
repository.getLowestPrice().then(lowest => {
|
||||
if (lowest) {
|
||||
connection.socket.send(JSON.stringify({
|
||||
type: 'index',
|
||||
data: {
|
||||
timestamp: lowest.timestamp,
|
||||
price: lowest.lowestPrice.toString(),
|
||||
vendor: lowest.vendor,
|
||||
seller: lowest.sellerName,
|
||||
},
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
158
backend/src/api/routes/prices.ts
Normal file
158
backend/src/api/routes/prices.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { PriceRepository } from '../../database/repository';
|
||||
|
||||
const repository = new PriceRepository();
|
||||
|
||||
export const priceRoutes: FastifyPluginAsync = async (server) => {
|
||||
// GET /api/prices/latest - Get all current listings
|
||||
server.get('/latest', async (request, reply) => {
|
||||
try {
|
||||
const prices = await repository.getLatestPrices();
|
||||
return {
|
||||
success: true,
|
||||
data: prices.map((p: any) => ({
|
||||
id: p.id,
|
||||
timestamp: p.timestamp,
|
||||
vendor: p.vendor,
|
||||
seller: p.sellerName,
|
||||
usdPrice: p.usdPrice.toString(),
|
||||
auecAmount: p.auecAmount.toString(),
|
||||
usdPerMillion: p.usdPerMillion.toString(),
|
||||
deliveryTime: p.deliveryTime,
|
||||
url: p.url,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
reply.code(500);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/prices/lowest - Get current lowest price
|
||||
server.get('/lowest', async (request, reply) => {
|
||||
try {
|
||||
const lowest = await repository.getLowestPrice();
|
||||
if (!lowest) {
|
||||
return { success: true, data: null };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
timestamp: lowest.timestamp,
|
||||
price: lowest.lowestPrice.toString(),
|
||||
vendor: lowest.vendor,
|
||||
seller: lowest.sellerName,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
reply.code(500);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/prices/by-seller?seller=&platform=
|
||||
server.get<{
|
||||
Querystring: { seller: string; platform?: string };
|
||||
}>('/by-seller', async (request, reply) => {
|
||||
try {
|
||||
const { seller, platform } = request.query;
|
||||
if (!seller) {
|
||||
reply.code(400);
|
||||
return { success: false, error: 'seller parameter is required' };
|
||||
}
|
||||
|
||||
const prices = await repository.getPricesBySeller(seller, platform);
|
||||
return {
|
||||
success: true,
|
||||
data: prices.map((p: any) => ({
|
||||
timestamp: p.timestamp,
|
||||
vendor: p.vendor,
|
||||
seller: p.sellerName,
|
||||
usdPerMillion: p.usdPerMillion.toString(),
|
||||
deliveryTime: p.deliveryTime,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
reply.code(500);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/prices/by-platform?platform=
|
||||
server.get<{
|
||||
Querystring: { platform: string };
|
||||
}>('/by-platform', async (request, reply) => {
|
||||
try {
|
||||
const { platform } = request.query;
|
||||
if (!platform) {
|
||||
reply.code(400);
|
||||
return { success: false, error: 'platform parameter is required' };
|
||||
}
|
||||
|
||||
const prices = await repository.getPricesByPlatform(platform);
|
||||
return {
|
||||
success: true,
|
||||
data: prices.map((p: any) => ({
|
||||
timestamp: p.timestamp,
|
||||
seller: p.sellerName,
|
||||
usdPerMillion: p.usdPerMillion.toString(),
|
||||
deliveryTime: p.deliveryTime,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
reply.code(500);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/prices/history?from=&to=&seller=&platform=
|
||||
server.get<{
|
||||
Querystring: { from: string; to: string; seller?: string; platform?: string };
|
||||
}>('/history', async (request, reply) => {
|
||||
try {
|
||||
const { from, to, seller, platform } = request.query;
|
||||
if (!from || !to) {
|
||||
reply.code(400);
|
||||
return { success: false, error: 'from and to parameters are required' };
|
||||
}
|
||||
|
||||
const fromDate = new Date(from);
|
||||
const toDate = new Date(to);
|
||||
|
||||
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) {
|
||||
reply.code(400);
|
||||
return { success: false, error: 'Invalid date format' };
|
||||
}
|
||||
|
||||
const prices = await repository.getPriceHistory(fromDate, toDate, seller, platform);
|
||||
return {
|
||||
success: true,
|
||||
data: prices.map((p: any) => ({
|
||||
timestamp: p.timestamp,
|
||||
vendor: p.vendor,
|
||||
seller: p.sellerName,
|
||||
usdPerMillion: p.usdPerMillion.toString(),
|
||||
deliveryTime: p.deliveryTime,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
reply.code(500);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
153
backend/src/api/routes/websocket.ts
Normal file
153
backend/src/api/routes/websocket.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { WebSocket } from 'ws';
|
||||
import { PriceRepository } from '../../database/repository';
|
||||
|
||||
interface WebSocketMessage {
|
||||
type: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
interface WebSocketConnection {
|
||||
socket: WebSocket;
|
||||
}
|
||||
|
||||
const clients = new Set<WebSocketConnection>();
|
||||
const repository = new PriceRepository();
|
||||
|
||||
export const websocketRoutes: FastifyPluginAsync = async (server) => {
|
||||
server.log.info('WebSocket route registered at /ws');
|
||||
|
||||
server.get('/ws', { websocket: true }, (socket: any, request: any) => {
|
||||
server.log.info('WebSocket client connected');
|
||||
console.log('✓ WebSocket client connected');
|
||||
|
||||
const connection = { socket };
|
||||
clients.add(connection);
|
||||
|
||||
// Send connection confirmation
|
||||
socket.send(JSON.stringify({
|
||||
type: 'connection_status',
|
||||
data: { connected: true, message: 'Connected to rmtPocketWatcher' }
|
||||
}));
|
||||
|
||||
// Handle incoming messages
|
||||
socket.on('message', async (message: Buffer) => {
|
||||
try {
|
||||
const parsed: WebSocketMessage = JSON.parse(message.toString());
|
||||
|
||||
if (parsed.type === 'subscribe' && parsed.data?.channel === 'price_updates') {
|
||||
// Send latest price data on subscription
|
||||
const latestPrices = await repository.getLatestPrices();
|
||||
|
||||
if (latestPrices.length > 0) {
|
||||
const lowestPrice = latestPrices.reduce((min: any, p: any) =>
|
||||
Number(p.usdPerMillion) < Number(min.usdPerMillion) ? p : min
|
||||
);
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
type: 'price_update',
|
||||
data: {
|
||||
timestamp: new Date(),
|
||||
lowestPrice: Number(lowestPrice.usdPerMillion),
|
||||
platform: lowestPrice.vendor,
|
||||
sellerName: lowestPrice.sellerName,
|
||||
allPrices: latestPrices.map((p: any) => ({
|
||||
id: p.id,
|
||||
platform: p.vendor,
|
||||
sellerName: p.sellerName,
|
||||
pricePerMillion: Number(p.usdPerMillion),
|
||||
timestamp: p.timestamp,
|
||||
url: p.url
|
||||
}))
|
||||
}
|
||||
}));
|
||||
}
|
||||
} else if (parsed.type === 'get_history') {
|
||||
// Handle historical data request
|
||||
const { range } = parsed.data || {};
|
||||
const now = new Date();
|
||||
let from = new Date();
|
||||
|
||||
switch (range) {
|
||||
case '6h':
|
||||
from = new Date(now.getTime() - 6 * 60 * 60 * 1000);
|
||||
break;
|
||||
case '24h':
|
||||
from = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
case '3d':
|
||||
from = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
case '7d':
|
||||
from = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
case '1mo':
|
||||
from = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
case 'ytd':
|
||||
from = new Date(now.getFullYear(), 0, 1);
|
||||
break;
|
||||
default:
|
||||
from = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
const history = await repository.getPriceHistory(from, now);
|
||||
|
||||
const payload = {
|
||||
type: 'history_data',
|
||||
data: {
|
||||
range,
|
||||
from,
|
||||
to: now,
|
||||
prices: history.map((p: any) => ({
|
||||
id: p.id,
|
||||
platform: p.vendor,
|
||||
sellerName: p.sellerName,
|
||||
pricePerMillion: Number(p.usdPerMillion),
|
||||
timestamp: p.timestamp,
|
||||
url: p.url
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
server.log.info(`Sending history data: ${payload.data.prices.length} prices for range ${range}`);
|
||||
socket.send(JSON.stringify(payload));
|
||||
}
|
||||
} catch (error: any) {
|
||||
server.log.error({ error }, 'Error handling WebSocket message');
|
||||
socket.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: 'Invalid message format'
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
server.log.info('WebSocket client disconnected');
|
||||
clients.delete(connection);
|
||||
});
|
||||
|
||||
socket.on('error', (error: any) => {
|
||||
server.log.error({ error }, 'WebSocket error');
|
||||
clients.delete(connection);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Broadcast price updates to all connected clients
|
||||
export function broadcastPriceUpdate(data: any): void {
|
||||
const message = JSON.stringify({
|
||||
type: 'price_update',
|
||||
data
|
||||
});
|
||||
|
||||
clients.forEach((connection) => {
|
||||
try {
|
||||
if (connection.socket.readyState === 1) { // OPEN
|
||||
connection.socket.send(message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error broadcasting to client:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
36
backend/src/api/server.ts
Normal file
36
backend/src/api/server.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import Fastify from 'fastify';
|
||||
import cors from '@fastify/cors';
|
||||
import websocket from '@fastify/websocket';
|
||||
import { priceRoutes } from './routes/prices';
|
||||
import { indexRoutes } from './routes/index';
|
||||
import { websocketRoutes } from './routes/websocket';
|
||||
|
||||
const server = Fastify({
|
||||
logger: {
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
},
|
||||
});
|
||||
|
||||
// Register plugins
|
||||
server.register(cors, {
|
||||
origin: true,
|
||||
});
|
||||
|
||||
server.register(websocket);
|
||||
|
||||
// Register routes
|
||||
server.register(priceRoutes, { prefix: '/api/prices' });
|
||||
server.register(indexRoutes, { prefix: '/api/index' });
|
||||
server.register(websocketRoutes);
|
||||
|
||||
// Health check
|
||||
server.get('/health', async () => {
|
||||
return { status: 'ok', timestamp: new Date().toISOString() };
|
||||
});
|
||||
|
||||
// Test endpoint to trigger scrape
|
||||
server.get('/api/test/scrape', async () => {
|
||||
return { message: 'Scrape will be triggered by scheduler' };
|
||||
});
|
||||
|
||||
export default server;
|
||||
Reference in New Issue
Block a user